ClamAVScan - antivirus scan mailet using ClamAV's CLAMD daemon
ClamAVScan does an antivirus scan check using the ClamAV daemon CLAMD (see http://www.clamav.net/).
It interacts directly with the daemon using the "stream" method, which should have the lowest possible overhead. I've done tests getting less than 2.5 seconds of CPU per megabyte scanned on a 1.5 GHz CPU.
The CLAMD daemon will typically reside on localhost, but could reside on a different host. It may also consist on a set of multiple daemons, each residing on a different server and on different IP number. In such case a DNS host name with multiple IP adresses (round-robin load sharing) is supported by the mailet (but on the same port number).
ClamAV runs on Linux, but there are ports to other OS too. I have ClamAVScan working on Windows XP and 2k using ClamAV For Windows (see http://www.sosdg.org/clamav-win32)
The init parameters are as follows:
<host>: the host name of the server where CLAMD runs. It can either be a machine name, such as "java.sun.com", or a textual representation of its IP address. If a literal IP address is supplied, only the validity of the address format is checked. If the machine name resolves to multiple IP addresses, round-robin load sharing will be used. The default is localhost.
<port>: the port on which CLAMD listens. The default is 3310.
<maxPings>: the maximum number of connection retries during startup. If the value is 0 no startup test will be done. The default is 6.
<pingIntervalMilli>: the interval between each connection retry during startup. The default is 30000 (30 seconds).
<streamBufferSize>: the BufferedOutputStream buffer size to use when writing to the stream connection. The default is 8192.
The actions performed are as follows:
- During initialization:
- Gets all config.xml parameters, handling the defaults;
resolves the <host> parameter, creating the round-robin IP list;
connects to CLAMD at the first IP in the round-robin list, on the specified <port>;
if unsuccessful, retries every <pingIntervalMilli> milliseconds up to <maxPings> times;
- sends a "PING" request;
- waits for a "PONG" answer;
- repeats steps 3-6 for every other IP resolved.
- For every mail
connects to CLAMD at the "next" IP in the round-robin list, on the specified <port>, and increments the "next" index; if the connection request is not accepted, tries with the next one in the list unless all of them have failed;
- sends a "STREAM" request;
parses the "PORT streamPort" answer obtaining the port number;
makes a second connection (the stream connection) to CLAMD at the same host (or IP) on the streamPort just obtained;
sends the MimeMessage to CLAMD (using MimeMessage#writeTo(OutputStream)) through the stream connection;
closes the stream connection;
- gets the "OK" or "... FOUND" answer from the main connection;
- closes the main connection;
sets the "org.apache.james.infected" mail attribute to either "true" or "false";
adds the "X-MessageIsInfected" header to either "true" or "false", depending on the results of the scan.
ClamAV configuration notes
The following parameters are required in clamav.conf:
LocalSocket must be commented out
TCPSocket must be set to a port# (typically 3310)
StreamMaxLength must be >= the James config.xml parameter <maxmessagesize> in SMTP <handler>
MaxThreads should? be >= the James config.xml parameter <threads> in <spoolmanager>
ScanMail must be uncommented
A James config.xml example
Here follows an example of config.xml definitions deploying CLAMD on localhost, and handling the infected messages:
... <!-- Do an antivirus scan --> <mailet match="All" class="ClamAVScan" onMailetException="ignore"/> <!-- If infected go to virus processor --> <mailet match="HasMailAttributeWithValue=org.apache.james.infected, true" class="ToProcessor"> <processor> virus </processor> </mailet> <!-- Check attachment extensions for possible viruses --> <mailet match="AttachmentFileNameIs=-d -z *.exe *.com *.bat *.cmd *.pif *.scr *.vbs *.avi *.mp3 *.mpeg *.shs" class="ToProcessor" onMatchException="noMatch"> <processor> bad-extensions </processor> </mailet> ... <!-- Messages containing viruses --> <processor name="virus"> <!-- To avoid a loop while bouncing --> <mailet match="All" class="SetMailAttribute"> <org.apache.james.infected>true, bouncing</org.apache.james.infected> </mailet> <mailet match="SMTPAuthSuccessful" class="Bounce"> <sender>email@example.com</sender> <inline>heads</inline> <attachment>none</attachment> <notice>Warning: We were unable to deliver the message below because it was found infected by virus(es).</notice> </mailet> <!-- <mailet match="All" class="ToRepository"> <repositoryPath>file://var/mail/infected/</repositoryPath> </mailet> --> <mailet match="All" class="Null"/> </processor> ...
The reason for the onMailetException="ignore" entry in the ClamAVScan mailet "call" is to avoid losing the message if an Exception is unlikely thrown, but it is just my choice, as I have a "second line" defense blocking "bad extensions" with an "AttachmentFileNameIs=" call.