View Javadoc

1   /*****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  package org.apache.james.transport.mailets;
21  
22  import org.apache.mailet.RFC2822Headers;
23  import org.apache.mailet.GenericMailet;
24  import org.apache.mailet.Mail;
25  
26  import javax.mail.MessagingException;
27  import javax.mail.internet.MimeMessage;
28  
29  import java.io.BufferedOutputStream;
30  import java.io.BufferedReader;
31  import java.io.BufferedWriter;
32  import java.io.IOException;
33  import java.io.InputStreamReader;
34  import java.io.OutputStreamWriter;
35  import java.io.PrintWriter;
36  import java.io.StringWriter;
37  import java.net.ConnectException;
38  import java.net.InetAddress;
39  import java.net.Socket;
40  import java.net.UnknownHostException;
41  import java.util.ArrayList;
42  import java.util.Collection;
43  import java.util.HashSet;
44  import java.util.Iterator;
45  import java.util.Set;
46  
47  
48  /***
49   * <P>Does an antivirus scan check using a ClamAV daemon (CLAMD)</P>
50   * 
51   * <P> Interacts directly with the daemon using the "stream" method,
52   * which should have the lowest possible overhead.</P>
53   * <P>The CLAMD daemon will typically reside on <I>localhost</I>, but could reside on a
54   * different host.
55   * It may also consist on a set of multiple daemons, each residing on a different
56   * server and on different IP number.
57   * In such case a DNS host name with multiple IP addresses (round-robin load sharing)
58   * is supported by the mailet (but on the same port number).</P>
59   * 
60   * <P>Handles the following init parameters:</P>
61   * <UL>
62   *    <LI><CODE>&lt;debug&gt;</CODE>.</LI>
63   *    <LI><CODE>&lt;host&gt;</CODE>: the host name of the server where CLAMD runs. It can either be
64   *        a machine name, such as
65   *        "<code>java.sun.com</code>", or a textual representation of its
66   *        IP address. If a literal IP address is supplied, only the
67   *        validity of the address format is checked.
68   *        If the machine name resolves to multiple IP addresses, <I>round-robin load sharing</I> will
69   *        be used.
70   *        The default is <CODE>localhost</CODE>.</LI>
71   *    <LI><CODE>&lt;port&gt;</CODE>: the port on which CLAMD listens. The default is <I>3310</I>.</LI>
72   *    <LI><CODE>&lt;maxPings&gt;</CODE>: the maximum number of connection retries during startup.
73   *        If the value is <I>0</I> no startup test will be done.
74   *        The default is <I>6</I>.</LI>
75   *    <LI><CODE>&lt;pingIntervalMilli&gt;</CODE>: the interval (in milliseconds)
76   *        between each connection retry during startup.
77   *        The default is <I>30000</I> (30 seconds).</LI>
78   *    <LI><CODE>&lt;streamBufferSize&gt;</CODE>: the BufferedOutputStream buffer size to use 
79   *        writing to the <I>stream connection</I>. The default is <I>8192</I>.</LI>
80   * </UL>
81   * 
82   * <P>The actions performed are as follows:</P>
83   * <UL>
84   *    <LI>During initialization:</LI>
85   *    <OL>
86   *        <LI>Gets all <CODE>config.xml</CODE> parameters, handling the defaults;</LI>
87   *        <LI>resolves the <CODE>&lt;host&gt;</CODE> parameter, creating the round-robin IP list;</LI>
88   *        <LI>connects to CLAMD at the first IP in the round-robin list, on
89   *            the specified <CODE>&lt;port&gt;</CODE>;</LI>
90   *        <LI>if unsuccessful, retries every <CODE>&lt;pingIntervalMilli&gt;</CODE> milliseconds up to
91   *            <CODE>&lt;maxPings&gt;</CODE> times;</LI>
92   *        <LI>sends a <CODE>PING</CODE> request;</LI>
93   *        <LI>waits for a <CODE>PONG</CODE> answer;</LI>
94   *        <LI>repeats steps 3-6 for every other IP resolved.
95   *    </OL>
96   *    <LI>For every mail</LI>
97   *    <OL>
98   *        <LI>connects to CLAMD at the "next" IP in the round-robin list, on
99   *            the specified <CODE>&lt;port&gt;</CODE>, and increments the "next" index;
100  *            if the connection request is not accepted tries with the next one
101  *            in the list unless all of them have failed;</LI>
102  *        <LI>sends a "<CODE>STREAM</CODE>" request;</LI>
103  *        <LI>parses the "<CODE>PORT <I>streamPort</I></CODE>" answer obtaining the port number;</LI>
104  *        <LI>makes a second connection (the <I>stream connection</I>) to CLAMD at the same host (or IP)
105  *            on the <I>streamPort</I> just obtained;</LI>
106  *        <LI>sends the mime message to CLAMD (using {@link MimeMessage#writeTo(OutputStream)})
107  *            through the <I>stream connection</I>;</LI>
108  *        <LI>closes the <I>stream connection</I>;</LI>
109  *        <LI>gets the "<CODE>OK</CODE>" or "<CODE>... FOUND</CODE>" answer from the main connection;</LI>
110  *        <LI>closes the main connection;</LI>
111  *        <LI>sets the "<CODE>org.apache.james.infected</CODE>" <I>mail attribute</I> to either
112  *            "<CODE>true</CODE>" or "<CODE>false</CODE>";</LI>
113  *        <LI>adds the "<CODE>X-MessageIsInfected</CODE>" <I>header</I> to either
114  *            "<CODE>true</CODE>" or "<CODE>false</CODE>";</LI>
115  *    </OL>
116  * </UL>
117  * 
118  * <P>Some notes regarding <a href="http://www.clamav.net/">clamav.conf</a>:</p>
119  * <UL>
120  *    <LI><CODE>LocalSocket</CODE> must be commented out</LI>
121  *    <LI><CODE>TCPSocket</CODE> must be set to a port# (typically 3310)</LI>
122  *    <LI><CODE>StreamMaxLength</CODE> must be &gt;= the James config.xml parameter
123  *    &lt;<CODE>maxmessagesize</CODE>&gt; in SMTP &lt;<CODE>handler</CODE>&gt;</LI>
124  *    <LI><CODE>MaxThreads</CODE> should? be &gt;= the James config.xml parameter
125  *    &lt;<CODE>threads</CODE>&gt; in &lt;<CODE>spoolmanager</CODE>&gt;</LI>
126  *    <LI><CODE>ScanMail</CODE> must be uncommented</LI>
127  * </UL>
128  *
129  * <P>Here follows an example of config.xml definitions deploying CLAMD on localhost,
130  * and handling the infected messages:</P>
131  * <PRE><CODE>
132  *
133  * ...
134  *
135  *    &lt;!-- Do an antivirus scan --&gt;
136  *    &lt;mailet match="All" class="ClamAVScan" onMailetException="ignore"/&gt;
137  *
138  *    &lt;!-- If infected go to virus processor --&gt;
139  *    &lt;mailet match="HasMailAttributeWithValue=org.apache.james.infected, true" class="ToProcessor"&gt;
140  *       &lt;processor&gt; virus &lt;/processor&gt;
141  *    &lt;/mailet&gt;
142  *
143  *    &lt;!-- Check attachment extensions for possible viruses --&gt;
144  *    &lt;mailet match="AttachmentFileNameIs=-d -z *.exe *.com *.bat *.cmd *.pif *.scr *.vbs *.avi *.mp3 *.mpeg *.shs" class="ToProcessor"&gt;
145  *       &lt;processor&gt; bad-extensions &lt;/processor&gt;
146  *    &lt;/mailet&gt;
147  *
148  * ...
149  *
150  * &lt;!-- Messages containing viruses --&gt;
151  * &lt;processor name="virus"&gt;
152  *
153  *    &lt;!-- To avoid a loop while bouncing --&gt;
154  *    &lt;mailet match="All" class="SetMailAttribute"&gt;
155  *       &lt;org.apache.james.infected&gt;true, bouncing&lt;/org.apache.james.infected&gt;
156  *    &lt;/mailet&gt;
157  *
158  *    &lt;mailet match="SMTPAuthSuccessful" class="Bounce"&gt;
159  *       &lt;sender&gt;bounce-admin@xxx.com&lt;/sender&gt;
160  *       &lt;inline&gt;heads&lt;/inline&gt;
161  *       &lt;attachment&gt;none&lt;/attachment&gt;
162  *       &lt;notice&gt; Warning: We were unable to deliver the message below because it was found infected by virus(es). &lt;/notice&gt;
163  *    &lt;/mailet&gt;
164  *
165  *    &lt;!--
166  *    &lt;mailet match="All" class="ToRepository"&gt;
167  *       &lt;repositoryPath&gt;file://var/mail/infected/&lt;/repositoryPath&gt;
168  *    &lt;/mailet&gt;
169  *    --&gt;
170  *
171  *    &lt;mailet match="All" class="Null" /&gt;
172  * &lt;/processor&gt;
173  * </CODE></PRE>
174  *
175  * @version 2.2.1
176  * @since 2.2.1
177  * @see <a href="http://www.clamav.net/">ClamAV Home Page</a>
178  * @see <a href="http://www.sosdg.org/clamav-win32/">ClamAV For Windows</a>
179  */
180 public class ClamAVScan extends GenericMailet {
181     
182     private static final int DEFAULT_PORT = 3310;
183     
184     private static final int DEFAULT_MAX_PINGS = 6;
185     
186     private static final int DEFAULT_PING_INTERVAL_MILLI = 30000;
187     
188     private static final int DEFAULT_STREAM_BUFFER_SIZE = 8192;
189     
190     private static final int DEFAULT_CONNECTION_TIMEOUT = 20000;
191 
192     private static final String STREAM_PORT_STRING = "PORT ";
193     
194     private static final String FOUND_STRING = "FOUND";
195     
196     private static final String MAIL_ATTRIBUTE_NAME = "org.apache.james.infected";
197     
198     private static final String HEADER_NAME = "X-MessageIsInfected";
199     
200     /***
201      * Holds value of property debug.
202      */
203     private boolean debug;
204     
205     /***
206      * Holds value of property host.
207      */
208     private String host;
209     
210     /***
211      * Holds value of property port.
212      */
213     private int port;
214     
215     /***
216      * Holds value of property maxPings.
217      */
218     private int maxPings;
219     
220     /***
221      * Holds value of property pingIntervalMilli.
222      */
223     private int pingIntervalMilli;
224     
225     /***
226      * Holds value of property streamBufferSize.
227      */
228     private int streamBufferSize;
229 
230     /***
231      * Holds value of property addresses.
232      */
233     private InetAddress[] addresses;
234     
235     /***
236      * Holds the index of the next address to connect to
237      */
238     private int nextAddressIndex;
239 
240     /***
241      * Return a string describing this mailet.
242      *
243      * @return a string describing this mailet
244      */
245     public String getMailetInfo() {
246         return "Antivirus Check using ClamAV (CLAMD)";
247     }
248     
249     /*** Gets the expected init parameters. */
250     protected  String[] getAllowedInitParameters() {
251         String[] allowedArray = {
252             //            "static",
253             "debug",
254                     "host",
255                     "port",
256                     "maxPings",
257                     "pingIntervalMilli",
258                     "streamBufferSize"
259         };
260         return allowedArray;
261     }
262     
263     /***
264      * Initializer for property debug.
265      */
266     protected void initDebug() {
267         String debugParam = getInitParameter("debug");
268         setDebug((debugParam == null) ? false : new Boolean(debugParam).booleanValue());
269     }
270     
271     /***
272      * Getter for property debug.
273      * @return Value of property debug.
274      */
275     public boolean isDebug() {
276         return this.debug;
277     }
278     
279     /***
280      * Setter for property debug.
281      * @param debug New value of property debug.
282      */
283     public void setDebug(boolean debug) {
284         this.debug = debug;
285     }
286     
287     /***
288      * Initializer for property host.
289      * @throws UnknownHostException if unable to resolve the host name, or if invalid
290      */
291     protected void initHost() throws UnknownHostException {
292         setHost(getInitParameter("host"));
293         if (isDebug()) {
294             log("host: " + getHost());
295         }
296     }
297     
298     /***
299      * Getter for property host.
300      * @return Value of property host.
301      */
302     public String getHost() {
303         
304         return this.host;
305     }
306     
307     /***
308      * Setter for property host.
309      * Resolves also the host name into the corresponding IP addresses, issues
310      * a {@link #setAddresses} and resets the <CODE>nextAddressIndex</CODE>
311      * variable to <I>0</I> for dealing with <I>round-robin</I>.
312      * @param host New value of property host.
313      * @throws UnknownHostException if unable to resolve the host name, or if invalid
314      */
315     public void setHost(String host) throws UnknownHostException {
316         
317         this.host = host;
318         
319         setAddresses(InetAddress.getAllByName(host));
320         
321         nextAddressIndex = 0;
322     }
323     
324     /***
325      * Initializer for property port.
326      */
327     protected void initPort() {
328         String portParam = getInitParameter("port");
329         setPort((portParam == null) ? DEFAULT_PORT : Integer.parseInt(portParam));
330         if (isDebug()) {
331             log("port: " + getPort());
332         }
333     }
334     
335     /***
336      * Getter for property port.
337      * @return Value of property port.
338      */
339     public int getPort() {
340         
341         return this.port;
342     }
343     
344     /***
345      * Setter for property port.
346      * @param port New value of property port.
347      */
348     public void setPort(int port) {
349         
350         this.port = port;
351     }
352     
353     /***
354      * Initializer for property maxPings.
355      */
356     protected void initMaxPings() {
357         String maxPingsParam = getInitParameter("maxPings");
358         setMaxPings((maxPingsParam == null) ? DEFAULT_MAX_PINGS : Integer.parseInt(maxPingsParam));
359         if (isDebug()) {
360             log("maxPings: " + getMaxPings());
361         }
362     }
363     
364     /***
365      * Getter for property maxPings.
366      * @return Value of property maxPings.
367      */
368     public int getMaxPings() {
369         
370         return this.maxPings;
371     }
372     
373     /***
374      * Setter for property maxPings.
375      * @param maxPings New value of property maxPings.
376      */
377     public void setMaxPings(int maxPings) {
378         
379         this.maxPings = maxPings;
380     }
381     
382     /***
383      * Initializer for property pingIntervalMilli.
384      */
385     protected void initPingIntervalMilli() {
386         String pingIntervalMilliParam = getInitParameter("pingIntervalMilli");
387         setPingIntervalMilli((pingIntervalMilliParam == null) ? DEFAULT_PING_INTERVAL_MILLI : Integer.parseInt(pingIntervalMilliParam));
388         if (isDebug()) {
389             log("pingIntervalMilli: " + getPingIntervalMilli());
390         }
391     }
392     
393     /***
394      * Getter for property pingIntervalMilli.
395      * @return Value of property pingIntervalMilli.
396      */
397     public int getPingIntervalMilli() {
398         
399         return this.pingIntervalMilli;
400     }
401     
402     /***
403      * Setter for property pingIntervalMilli.
404      * @param pingIntervalMilli New value of property pingIntervalMilli.
405      */
406     public void setPingIntervalMilli(int pingIntervalMilli) {
407         
408         this.pingIntervalMilli = pingIntervalMilli;
409     }
410     
411     /***
412      * Initializer for property streamBufferSize.
413      */
414     protected void initStreamBufferSize() {
415         String streamBufferSizeParam = getInitParameter("streamBufferSize");
416         setStreamBufferSize((streamBufferSizeParam == null) ? DEFAULT_STREAM_BUFFER_SIZE : Integer.parseInt(streamBufferSizeParam));
417         if (isDebug()) {
418             log("streamBufferSize: " + getStreamBufferSize());
419         }
420     }
421     
422     /***
423      * Getter for property streamBufferSize.
424      * @return Value of property streamBufferSize.
425      */
426     public int getStreamBufferSize() {
427         
428         return this.streamBufferSize;
429     }
430     
431     /***
432      * Setter for property streamBufferSize.
433      * @param streamBufferSize New value of property streamBufferSize.
434      */
435     public void setStreamBufferSize(int streamBufferSize) {
436         
437         this.streamBufferSize = streamBufferSize;
438     }
439     
440     /***
441      * Indexed getter for property addresses.
442      * @param index Index of the property.
443      * @return Value of the property at <CODE>index</CODE>.
444      */
445     protected InetAddress getAddresses(int index) {
446 
447         return this.addresses[index];
448     }
449 
450     /***
451      * Getter for property addresses.
452      * @return Value of property addresses.
453      */
454     protected InetAddress[] getAddresses() {
455 
456         return this.addresses;
457     }
458 
459     /***
460      * Setter for property addresses.
461      * @param addresses New value of property addresses.
462      */
463     protected void setAddresses(InetAddress[] addresses) {
464 
465         this.addresses = addresses;
466     }
467     
468     /***
469      * Getter for property nextAddress.
470      * 
471      * Gets the address of the next CLAMD server to connect to in this round, using round-robin.
472      * Increments the nextAddressIndex for the next round.
473      * @return Value of property address.
474      */
475     protected synchronized InetAddress getNextAddress() {
476         
477         InetAddress address = getAddresses(nextAddressIndex);
478         
479         nextAddressIndex++;
480         if (nextAddressIndex >= getAddressesCount()) {
481             nextAddressIndex = 0;
482         }
483         
484         return address;
485     }
486 
487     /***
488      * Getter for property addressesCount.
489      * @return Value of property addressesCount.
490      */
491     public int getAddressesCount() {
492         return getAddresses().length;
493     }
494 
495     /***
496      * Gets a Socket connected to CLAMD.
497      * 
498      * Will loop though the round-robin address list until the first one accepts
499      * the connection.
500      * @return a socket connected to CLAMD
501      * @throws MessagingException if no CLAMD in the round-robin address list has accepted the connection
502      */
503     protected Socket getClamdSocket() throws MessagingException {
504         
505         InetAddress address = null;
506         
507         Set usedAddresses = new HashSet(getAddressesCount());
508         for (;;) {
509             // this do-while loop is needed because other threads could in the meantime
510             // calling getNextAddress(), and because of that the current thread may skip
511             // some working address
512             do {
513                 if (usedAddresses.size() >= getAddressesCount()) {
514                     String logText = "Unable to connect to CLAMD. All addresses failed.";
515                     log(logText + " Giving up.");
516                     throw new MessagingException(logText);
517                 }
518                 address = getNextAddress();
519             } while (!usedAddresses.add(address));
520             try {
521                 // get the socket
522                 return new Socket(address, getPort());
523             } catch (IOException ioe) {
524                 log("Exception caught acquiring main socket to CLAMD on "
525                         + address + " on port " + getPort() + ": " + ioe.getMessage());
526                 address = getNextAddress();
527                 // retry
528                 continue;
529             }
530         }
531     }
532     
533     /***
534      * Mailet initialization routine.
535      */
536     public void init() throws MessagingException {
537         
538         // check that all init parameters have been declared in allowedInitParameters
539         checkInitParameters(getAllowedInitParameters());
540         
541         try {
542             initDebug();
543             if (isDebug()) {
544                 log("Initializing");
545             }
546             
547             initHost();
548             initPort();
549             initMaxPings();
550             initPingIntervalMilli();
551             initStreamBufferSize();
552             
553             // If "maxPings is > ping the CLAMD server to check if it is up
554             if (getMaxPings() > 0) {
555                 ping();
556             }
557             
558         } catch (Exception e) {
559             log("Exception thrown", e);
560             throw new MessagingException("Exception thrown", e);
561         }
562         
563     }
564     
565     /***
566      * Scans the mail.
567      *
568      * @param mail the mail to scan
569      * @throws MessagingException if a problem arises
570      */
571     public void service(Mail mail) throws MessagingException {
572         
573         // if already checked no action
574         if (mail.getAttribute(MAIL_ATTRIBUTE_NAME) != null) {
575             return;
576         }
577         
578         MimeMessage mimeMessage = mail.getMessage();
579         
580         if (mimeMessage == null) {
581             log("Null MimeMessage. Will send to ghost");
582             // write mail info to log
583             logMailInfo(mail);
584             mail.setState(Mail.GHOST);
585             return;
586         }
587         
588         // get the socket
589         Socket socket = getClamdSocket();
590         BufferedReader reader = null;
591         PrintWriter writer = null;
592         Socket streamSocket = null;
593         BufferedOutputStream bos = null;
594             
595         try {
596             
597             // prepare the reader and writer for the commands
598             reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ASCII"));
599             writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
600             
601             // write a request for a port to use for streaming out the data to scan
602             writer.println("STREAM");
603             writer.flush();
604             
605             // parse and get the "stream" port#
606             int streamPort = getStreamPortFromAnswer(reader.readLine());
607             
608             // get the "stream" socket and the related (buffered) output stream
609             streamSocket = new Socket(socket.getInetAddress(), streamPort);
610             bos = new BufferedOutputStream(streamSocket.getOutputStream(), getStreamBufferSize());
611             
612             // stream out the message to the scanner
613             mimeMessage.writeTo(bos);
614             bos.flush();
615             bos.close();
616             streamSocket.close();
617             
618             String answer = null;
619             boolean virusFound = false;
620             String logMessage = "";
621             for (;;) {
622                 answer = reader.readLine();
623                 if (answer != null) {
624                     answer = answer.trim();
625                     
626                     // if a virus is found the answer will be '... FOUND'
627                     if (answer.substring(answer.length() - FOUND_STRING.length()).equals(FOUND_STRING)) {
628                         virusFound = true;
629                         logMessage = answer + " (by CLAMD on " + socket.getInetAddress() + ")";
630                         log(logMessage);
631                     }
632                 } else {
633                     break;
634                 }
635             }
636             
637             reader.close();
638             writer.close();
639             
640             if (virusFound) {
641                 String errorMessage = mail.getErrorMessage();
642                 if (errorMessage == null) {
643                     errorMessage = "";
644                 } else {
645                     errorMessage += "\r\n";
646                 }
647                 StringBuffer sb = new StringBuffer(errorMessage);
648                 sb.append(logMessage + "\r\n");
649                 
650                 // write mail and message info to log
651                 logMailInfo(mail);
652                 logMessageInfo(mimeMessage);
653                 
654                 // mark the mail with a mail attribute to check later on by other matchers/mailets
655                 mail.setAttribute(MAIL_ATTRIBUTE_NAME, "true");
656                 
657                 // sets the error message to be shown in any "notifyXxx" message
658                 mail.setErrorMessage(sb.toString());
659                 
660                 // mark the message with a header string
661                 mimeMessage.setHeader(HEADER_NAME, "true");
662                 
663             } else {
664                 if (isDebug()) {
665                     log("OK (by CLAMD on " + socket.getInetAddress() + ")");
666                 }
667                 mail.setAttribute(MAIL_ATTRIBUTE_NAME, "false");
668                 
669                 // mark the message with a header string
670                 mimeMessage.setHeader(HEADER_NAME, "false");
671                 
672             }
673             
674             try {
675                 saveChanges(mimeMessage);
676             } catch (Exception ex) {
677                 log("Exception caught while saving changes (header) to the MimeMessage. Ignoring ...", ex);
678             }
679             
680         } catch (Exception ex) {
681             log("Exception caught calling CLAMD on " + socket.getInetAddress() + ": " + ex.getMessage(), ex);
682             throw new MessagingException("Exception caught", ex);
683         } finally {
684             try {
685                 if (reader != null) {
686                     reader.close();
687                 }
688             } catch (Throwable t) {}
689             try {
690                 if (writer != null) {
691                     writer.close();
692                 }
693             } catch (Throwable t) {}
694             try {
695                 if (bos != null) {
696                     bos.close();
697                 }
698             } catch (Throwable t) {}
699             try {
700                 if (streamSocket != null) {
701                     streamSocket.close();
702                 }
703             } catch (Throwable t) {}
704             try {
705                 if (socket != null) {
706                     socket.close();
707                 }
708             } catch (Throwable t) {}
709         }
710         
711     }
712     
713     /***
714      * Checks if there are unallowed init parameters specified in the configuration file
715      * against the String[] allowedInitParameters.
716      * @param allowedArray array of strings containing the allowed parameter names
717      * @throws MessagingException if an unknown parameter name is found
718      */
719     protected final void checkInitParameters(String[] allowedArray) throws MessagingException {
720         // if null then no check is requested
721         if (allowedArray == null) {
722             return;
723         }
724         
725         Collection allowed = new HashSet();
726         Collection bad = new ArrayList();
727         
728         for (int i = 0; i < allowedArray.length; i++) {
729             allowed.add(allowedArray[i]);
730         }
731         
732         Iterator iterator = getInitParameterNames();
733         while (iterator.hasNext()) {
734             String parameter = (String) iterator.next();
735             if (!allowed.contains(parameter)) {
736                 bad.add(parameter);
737             }
738         }
739         
740         if (bad.size() > 0) {
741             throw new MessagingException("Unexpected init parameters found: "
742                     + arrayToString(bad.toArray()));
743         }
744     }
745     
746     /***
747      * Utility method for obtaining a string representation of an array of Objects.
748      */
749     private final String arrayToString(Object[] array) {
750         if (array == null) {
751             return "null";
752         }
753         StringBuffer sb = new StringBuffer(1024);
754         sb.append("[");
755         for (int i = 0; i < array.length; i++) {
756             if (i > 0) {
757                 sb.append(",");
758             }
759             sb.append(array[i]);
760         }
761         sb.append("]");
762         return sb.toString();
763     }
764     
765     /***
766      * Tries to "ping" all the CLAMD daemons to
767      * check if they are up and accepting requests.
768      **/
769     
770     protected void ping() throws Exception {
771         
772         for (int i = 0; i < getAddressesCount(); i++) {
773             ping(getAddresses(i));
774         }
775     }
776     
777     /***
778      * Tries (and retries as specified up to 'getMaxPings()') to "ping" the specified CLAMD daemon to
779      * check if it is up and accepting requests.
780      * @param address the address to "ping"
781      */
782     protected void ping(InetAddress address) throws Exception {
783         Socket socket = null;
784         
785         int ping = 1;
786         for (; ; ) {
787             if (isDebug()) {
788                 log("Trial #" + ping + "/" + getMaxPings() + " - creating socket connected to " + address + " on port " + getPort());
789             }
790             try {
791                 socket = new Socket(address, getPort());
792                 break;
793             } catch (ConnectException ce) {
794                 log("Trial #" + ping + "/" + getMaxPings() + " - exception caught: " + ce.toString() + " while creating socket connected to " + address + " on port " + getPort());
795                 ping++;
796                 if (ping <= getMaxPings()) {
797                     log("Waiting " + getPingIntervalMilli() + " milliseconds before retrying ...");
798                     Thread.sleep(getPingIntervalMilli());
799                 } else {
800                     break;
801                 }
802             }
803         }
804         
805         // if 'socket' is still null then 'maxPings' has been exceeded
806         if (socket == null) {
807             throw new ConnectException("maxPings exceeded: " + getMaxPings() + ". Giving up. The clamd daemon seems not to be running");
808         }
809         
810         try {
811             // get the reader and writer to ping and receive pong
812             BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ASCII"));
813             PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
814             
815             log("Sending: \"PING\" to " + address + " ...");
816             writer.println("PING");
817             writer.flush();
818             
819             boolean pongReceived = false;
820             for (;;) {
821                 String answer = reader.readLine();
822                 if (answer != null) {
823                     answer = answer.trim();
824                     log("Received: \"" + answer + "\"");
825                     answer = answer.trim();
826                     if (answer.equals("PONG")) {
827                         pongReceived = true;
828                     }
829                     
830                 } else {
831                     break;
832                 }
833             }
834             
835             reader.close();
836             writer.close();
837             
838             if (!pongReceived) {
839                 throw new ConnectException("Bad answer from \"PING\" probe: expecting \"PONG\"");
840             }
841         } finally {
842             socket.close();
843         }
844     }
845     
846     /***
847      * Parses the answer from a STREAM request and gets the port number.
848      *
849      * @param answer the answer from CLAMD containing the port number
850      * @return the port number for streaming out the data to scan
851      */
852     protected final int getStreamPortFromAnswer(String answer) throws ConnectException {
853         int port = -1;
854         if (answer != null && answer.startsWith(STREAM_PORT_STRING)) {
855             try {
856                 port = Integer.parseInt(answer.substring(STREAM_PORT_STRING.length()));
857             } catch (NumberFormatException nfe) {
858                 
859             }
860         }
861         
862         if (port <= 0) {
863             throw new ConnectException("\"PORT nn\" expected - unable to parse: " + "\"" + answer + "\"");
864         }
865         
866         return port;
867     }
868     
869     /***
870      * Saves changes resetting the original message id.
871      *
872      * @param message the message to save
873      */
874     protected final void saveChanges(MimeMessage message) throws MessagingException {
875         String messageId = message.getMessageID();
876         message.saveChanges();
877         if (messageId != null) {
878             message.setHeader(RFC2822Headers.MESSAGE_ID, messageId);
879         }
880     }
881 
882     private void logMailInfo(Mail mail) {
883         
884         // writes the error message to the log
885         StringWriter sout = new StringWriter();
886         PrintWriter out = new PrintWriter(sout, true);
887         
888         out.print("Mail details:");
889         out.print(" MAIL FROM: " + mail.getSender());
890         Iterator rcptTo = mail.getRecipients().iterator();
891         out.print(", RCPT TO: " + rcptTo.next());
892         while (rcptTo.hasNext()) {
893             out.print(", " + rcptTo.next());
894         }
895                 
896         log(sout.toString());
897     }
898     
899     private void logMessageInfo(MimeMessage mimeMessage) {
900         
901         // writes the error message to the log
902         StringWriter sout = new StringWriter();
903         PrintWriter out = new PrintWriter(sout, true);
904         
905         out.println("MimeMessage details:");
906         
907         try {
908             if (mimeMessage.getSubject() != null) {
909                 out.println("  Subject: " + mimeMessage.getSubject());
910             }
911             if (mimeMessage.getSentDate() != null) {
912                 out.println("  Sent date: " + mimeMessage.getSentDate());
913             }
914             String[] sender = null;
915             sender = mimeMessage.getHeader(RFC2822Headers.FROM);
916             if (sender != null) {
917                 out.print("  From: ");
918                 for (int i = 0; i < sender.length; i++) {
919                     out.print(sender[i] + " ");
920                 }
921                 out.println();
922             }
923             String[] rcpts = null;
924             rcpts = mimeMessage.getHeader(RFC2822Headers.TO);
925             if (rcpts != null) {
926                 out.print("  To: ");
927                 for (int i = 0; i < rcpts.length; i++) {
928                     out.print(rcpts[i] + " ");
929                 }
930                 out.println();
931             }
932             rcpts = mimeMessage.getHeader(RFC2822Headers.CC);
933             if (rcpts != null) {
934                 out.print("  CC: ");
935                 for (int i = 0; i < rcpts.length; i++) {
936                     out.print(rcpts[i] + " ");
937                 }
938                 out.println();
939             }
940             out.print("  Size (in bytes): " + mimeMessage.getSize());
941             if (mimeMessage.getLineCount() >= 0) {
942                 out.print(", Number of lines: " + mimeMessage.getLineCount());
943             }
944         } catch (MessagingException  me) {
945             log("Exception caught reporting message details", me);
946         }
947         
948         log(sout.toString());
949     }
950 
951 }
952