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><debug></CODE>.</LI>
66 * <LI><CODE><host></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><port></CODE>: the port on which CLAMD listens. The default is <I>3310</I>.</LI>
75 * <LI><CODE><maxPings></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><pingIntervalMilli></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><streamBufferSize></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><host></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><port></CODE>;</LI>
93 * <LI>if unsuccessful, retries every <CODE><pingIntervalMilli></CODE> milliseconds up to
94 * <CODE><maxPings></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><port></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 >= the James config.xml parameter
126 * <<CODE>maxmessagesize</CODE>> in SMTP <<CODE>handler</CODE>></LI>
127 * <LI><CODE>MaxThreads</CODE> should? be >= the James config.xml parameter
128 * <<CODE>threads</CODE>> in <<CODE>spoolmanager</CODE>></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 * <!-- Do an antivirus scan -->
139 * <mailet match="All" class="ClamAVScan" onMailetException="ignore"/>
140 *
141 * <!-- If infected go to virus processor -->
142 * <mailet match="HasMailAttributeWithValue=org.apache.james.infected, true" class="ToProcessor">
143 * <processor> virus </processor>
144 * </mailet>
145 *
146 * <!-- Check attachment extensions for possible viruses -->
147 * <mailet match="AttachmentFileNameIs=-d -z *.exe *.com *.bat *.cmd *.pif *.scr *.vbs *.avi *.mp3 *.mpeg *.shs" class="ToProcessor">
148 * <processor> bad-extensions </processor>
149 * </mailet>
150 *
151 * ...
152 *
153 * <!-- Messages containing viruses -->
154 * <processor name="virus">
155 *
156 * <!-- To avoid a loop while bouncing -->
157 * <mailet match="All" class="SetMailAttribute">
158 * <org.apache.james.infected>true, bouncing</org.apache.james.infected>
159 * </mailet>
160 *
161 * <mailet match="SMTPAuthSuccessful" class="Bounce">
162 * <sender>bounce-admin@xxx.com</sender>
163 * <inline>heads</inline>
164 * <attachment>none</attachment>
165 * <notice> Warning: We were unable to deliver the message below because it was found infected by virus(es). </notice>
166 * </mailet>
167 *
168 * <!--
169 * <mailet match="All" class="ToRepository">
170 * <repositoryPath>file://var/mail/infected/</repositoryPath>
171 * </mailet>
172 * -->
173 *
174 * <mailet match="All" class="Null" />
175 * </processor>
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