View Javadoc

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