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