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