FileDocCategorySizeDatePackage
ClamAVScan.javaAPI DocApache James 2.3.133199Fri Jan 12 12:56:28 GMT 2007org.apache.james.transport.mailets

ClamAVScan

public class ClamAVScan extends org.apache.mailet.GenericMailet

Does an antivirus scan check using a ClamAV daemon (CLAMD)

Interacts directly with the daemon using the "stream" method, which should have the lowest possible overhead.

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 addresses (round-robin load sharing) is supported by the mailet (but on the same port number).

Handles the following init parameters:

  • <debug>.
  • <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 (in milliseconds) between each connection retry during startup. The default is 30000 (30 seconds).
  • <streamBufferSize>: the BufferedOutputStream buffer size to use writing to the stream connection. The default is 8192.

The actions performed are as follows:

  • During initialization:
    1. Gets all config.xml parameters, handling the defaults;
    2. resolves the <host> parameter, creating the round-robin IP list;
    3. connects to CLAMD at the first IP in the round-robin list, on the specified <port>;
    4. if unsuccessful, retries every <pingIntervalMilli> milliseconds up to <maxPings> times;
    5. sends a PING request;
    6. waits for a PONG answer;
    7. repeats steps 3-6 for every other IP resolved.
  • For every mail
    1. 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;
    2. sends a "STREAM" request;
    3. parses the "PORT streamPort" answer obtaining the port number;
    4. makes a second connection (the stream connection) to CLAMD at the same host (or IP) on the streamPort just obtained;
    5. sends the mime message to CLAMD (using {@link MimeMessage#writeTo(OutputStream)}) through the stream connection;
    6. closes the stream connection;
    7. gets the "OK" or "... FOUND" answer from the main connection;
    8. closes the main connection;
    9. sets the "org.apache.james.infected" mail attribute to either "true" or "false";
    10. adds the "X-MessageIsInfected" header to either "true" or "false";

Some notes regarding 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

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">
<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>bounce-admin@xxx.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>
version
2.2.1
since
2.2.1
see
ClamAV Home Page
see
ClamAV For Windows

Fields Summary
private static final int
DEFAULT_PORT
private static final int
DEFAULT_MAX_PINGS
private static final int
DEFAULT_PING_INTERVAL_MILLI
private static final int
DEFAULT_STREAM_BUFFER_SIZE
private static final int
DEFAULT_CONNECTION_TIMEOUT
private static final String
STREAM_PORT_STRING
private static final String
FOUND_STRING
private static final String
MAIL_ATTRIBUTE_NAME
private static final String
HEADER_NAME
private boolean
debug
Holds value of property debug.
private String
host
Holds value of property host.
private int
port
Holds value of property port.
private int
maxPings
Holds value of property maxPings.
private int
pingIntervalMilli
Holds value of property pingIntervalMilli.
private int
streamBufferSize
Holds value of property streamBufferSize.
private InetAddress[]
addresses
Holds value of property addresses.
private int
nextAddressIndex
Holds the index of the next address to connect to
Constructors Summary
Methods Summary
private final java.lang.StringarrayToString(java.lang.Object[] array)
Utility method for obtaining a string representation of an array of Objects.

        if (array == null) {
            return "null";
        }
        StringBuffer sb = new StringBuffer(1024);
        sb.append("[");
        for (int i = 0; i < array.length; i++) {
            if (i > 0) {
                sb.append(",");
            }
            sb.append(array[i]);
        }
        sb.append("]");
        return sb.toString();
    
protected final voidcheckInitParameters(java.lang.String[] allowedArray)
Checks if there are unallowed init parameters specified in the configuration file against the String[] allowedInitParameters.

param
allowedArray array of strings containing the allowed parameter names
throws
MessagingException if an unknown parameter name is found

        // if null then no check is requested
        if (allowedArray == null) {
            return;
        }
        
        Collection allowed = new HashSet();
        Collection bad = new ArrayList();
        
        for (int i = 0; i < allowedArray.length; i++) {
            allowed.add(allowedArray[i]);
        }
        
        Iterator iterator = getInitParameterNames();
        while (iterator.hasNext()) {
            String parameter = (String) iterator.next();
            if (!allowed.contains(parameter)) {
                bad.add(parameter);
            }
        }
        
        if (bad.size() > 0) {
            throw new MessagingException("Unexpected init parameters found: "
                    + arrayToString(bad.toArray()));
        }
    
protected java.net.InetAddressgetAddresses(int index)
Indexed getter for property addresses.

param
index Index of the property.
return
Value of the property at index.


        return this.addresses[index];
    
protected java.net.InetAddress[]getAddresses()
Getter for property addresses.

return
Value of property addresses.


        return this.addresses;
    
public intgetAddressesCount()
Getter for property addressesCount.

return
Value of property addressesCount.

        return getAddresses().length;
    
protected java.lang.String[]getAllowedInitParameters()
Gets the expected init parameters.

        String[] allowedArray = {
            //            "static",
            "debug",
                    "host",
                    "port",
                    "maxPings",
                    "pingIntervalMilli",
                    "streamBufferSize"
        };
        return allowedArray;
    
protected java.net.SocketgetClamdSocket()
Gets a Socket connected to CLAMD. Will loop though the round-robin address list until the first one accepts the connection.

return
a socket connected to CLAMD
throws
MessagingException if no CLAMD in the round-robin address list has accepted the connection

        
        InetAddress address = null;
        
        Set usedAddresses = new HashSet(getAddressesCount());
        for (;;) {
            // this do-while loop is needed because other threads could in the meantime
            // calling getNextAddress(), and because of that the current thread may skip
            // some working address
            do {
                if (usedAddresses.size() >= getAddressesCount()) {
                    String logText = "Unable to connect to CLAMD. All addresses failed.";
                    log(logText + " Giving up.");
                    throw new MessagingException(logText);
                }
                address = getNextAddress();
            } while (!usedAddresses.add(address));
            try {
                // get the socket
                return new Socket(address, getPort());
            } catch (IOException ioe) {
                log("Exception caught acquiring main socket to CLAMD on "
                        + address + " on port " + getPort() + ": " + ioe.getMessage());
                address = getNextAddress();
                // retry
                continue;
            }
        }
    
public java.lang.StringgetHost()
Getter for property host.

return
Value of property host.

        
        return this.host;
    
public java.lang.StringgetMailetInfo()
Return a string describing this mailet.

return
a string describing this mailet


                     
       
        return "Antivirus Check using ClamAV (CLAMD)";
    
public intgetMaxPings()
Getter for property maxPings.

return
Value of property maxPings.

        
        return this.maxPings;
    
protected synchronized java.net.InetAddressgetNextAddress()
Getter for property nextAddress. Gets the address of the next CLAMD server to connect to in this round, using round-robin. Increments the nextAddressIndex for the next round.

return
Value of property address.

        
        InetAddress address = getAddresses(nextAddressIndex);
        
        nextAddressIndex++;
        if (nextAddressIndex >= getAddressesCount()) {
            nextAddressIndex = 0;
        }
        
        return address;
    
public intgetPingIntervalMilli()
Getter for property pingIntervalMilli.

return
Value of property pingIntervalMilli.

        
        return this.pingIntervalMilli;
    
public intgetPort()
Getter for property port.

return
Value of property port.

        
        return this.port;
    
public intgetStreamBufferSize()
Getter for property streamBufferSize.

return
Value of property streamBufferSize.

        
        return this.streamBufferSize;
    
protected final intgetStreamPortFromAnswer(java.lang.String answer)
Parses the answer from a STREAM request and gets the port number.

param
answer the answer from CLAMD containing the port number
return
the port number for streaming out the data to scan

        int port = -1;
        if (answer != null && answer.startsWith(STREAM_PORT_STRING)) {
            try {
                port = Integer.parseInt(answer.substring(STREAM_PORT_STRING.length()));
            } catch (NumberFormatException nfe) {
                
            }
        }
        
        if (port <= 0) {
            throw new ConnectException("\"PORT nn\" expected - unable to parse: " + "\"" + answer + "\"");
        }
        
        return port;
    
public voidinit()
Mailet initialization routine.

        
        // check that all init parameters have been declared in allowedInitParameters
        checkInitParameters(getAllowedInitParameters());
        
        try {
            initDebug();
            if (isDebug()) {
                log("Initializing");
            }
            
            initHost();
            initPort();
            initMaxPings();
            initPingIntervalMilli();
            initStreamBufferSize();
            
            // If "maxPings is > ping the CLAMD server to check if it is up
            if (getMaxPings() > 0) {
                ping();
            }
            
        } catch (Exception e) {
            log("Exception thrown", e);
            throw new MessagingException("Exception thrown", e);
        }
        
    
protected voidinitDebug()
Initializer for property debug.

        String debugParam = getInitParameter("debug");
        setDebug((debugParam == null) ? false : new Boolean(debugParam).booleanValue());
    
protected voidinitHost()
Initializer for property host.

throws
UnknownHostException if unable to resolve the host name, or if invalid

        setHost(getInitParameter("host"));
        if (isDebug()) {
            log("host: " + getHost());
        }
    
protected voidinitMaxPings()
Initializer for property maxPings.

        String maxPingsParam = getInitParameter("maxPings");
        setMaxPings((maxPingsParam == null) ? DEFAULT_MAX_PINGS : Integer.parseInt(maxPingsParam));
        if (isDebug()) {
            log("maxPings: " + getMaxPings());
        }
    
protected voidinitPingIntervalMilli()
Initializer for property pingIntervalMilli.

        String pingIntervalMilliParam = getInitParameter("pingIntervalMilli");
        setPingIntervalMilli((pingIntervalMilliParam == null) ? DEFAULT_PING_INTERVAL_MILLI : Integer.parseInt(pingIntervalMilliParam));
        if (isDebug()) {
            log("pingIntervalMilli: " + getPingIntervalMilli());
        }
    
protected voidinitPort()
Initializer for property port.

        String portParam = getInitParameter("port");
        setPort((portParam == null) ? DEFAULT_PORT : Integer.parseInt(portParam));
        if (isDebug()) {
            log("port: " + getPort());
        }
    
protected voidinitStreamBufferSize()
Initializer for property streamBufferSize.

        String streamBufferSizeParam = getInitParameter("streamBufferSize");
        setStreamBufferSize((streamBufferSizeParam == null) ? DEFAULT_STREAM_BUFFER_SIZE : Integer.parseInt(streamBufferSizeParam));
        if (isDebug()) {
            log("streamBufferSize: " + getStreamBufferSize());
        }
    
public booleanisDebug()
Getter for property debug.

return
Value of property debug.

        return this.debug;
    
private voidlogMailInfo(org.apache.mailet.Mail mail)

        
        // writes the error message to the log
        StringWriter sout = new StringWriter();
        PrintWriter out = new PrintWriter(sout, true);
        
        out.print("Mail details:");
        out.print(" MAIL FROM: " + mail.getSender());
        Iterator rcptTo = mail.getRecipients().iterator();
        out.print(", RCPT TO: " + rcptTo.next());
        while (rcptTo.hasNext()) {
            out.print(", " + rcptTo.next());
        }
                
        log(sout.toString());
    
private voidlogMessageInfo(javax.mail.internet.MimeMessage mimeMessage)

        
        // writes the error message to the log
        StringWriter sout = new StringWriter();
        PrintWriter out = new PrintWriter(sout, true);
        
        out.println("MimeMessage details:");
        
        try {
            if (mimeMessage.getSubject() != null) {
                out.println("  Subject: " + mimeMessage.getSubject());
            }
            if (mimeMessage.getSentDate() != null) {
                out.println("  Sent date: " + mimeMessage.getSentDate());
            }
            String[] sender = null;
            sender = mimeMessage.getHeader(RFC2822Headers.FROM);
            if (sender != null) {
                out.print("  From: ");
                for (int i = 0; i < sender.length; i++) {
                    out.print(sender[i] + " ");
                }
                out.println();
            }
            String[] rcpts = null;
            rcpts = mimeMessage.getHeader(RFC2822Headers.TO);
            if (rcpts != null) {
                out.print("  To: ");
                for (int i = 0; i < rcpts.length; i++) {
                    out.print(rcpts[i] + " ");
                }
                out.println();
            }
            rcpts = mimeMessage.getHeader(RFC2822Headers.CC);
            if (rcpts != null) {
                out.print("  CC: ");
                for (int i = 0; i < rcpts.length; i++) {
                    out.print(rcpts[i] + " ");
                }
                out.println();
            }
            out.print("  Size (in bytes): " + mimeMessage.getSize());
            if (mimeMessage.getLineCount() >= 0) {
                out.print(", Number of lines: " + mimeMessage.getLineCount());
            }
        } catch (MessagingException  me) {
            log("Exception caught reporting message details", me);
        }
        
        log(sout.toString());
    
protected voidping()
Tries to "ping" all the CLAMD daemons to check if they are up and accepting requests.

        
        for (int i = 0; i < getAddressesCount(); i++) {
            ping(getAddresses(i));
        }
    
protected voidping(java.net.InetAddress address)
Tries (and retries as specified up to 'getMaxPings()') to "ping" the specified CLAMD daemon to check if it is up and accepting requests.

param
address the address to "ping"

        Socket socket = null;
        
        int ping = 1;
        for (; ; ) {
            if (isDebug()) {
                log("Trial #" + ping + "/" + getMaxPings() + " - creating socket connected to " + address + " on port " + getPort());
            }
            try {
                socket = new Socket(address, getPort());
                break;
            } catch (ConnectException ce) {
                log("Trial #" + ping + "/" + getMaxPings() + " - exception caught: " + ce.toString() + " while creating socket connected to " + address + " on port " + getPort());
                ping++;
                if (ping <= getMaxPings()) {
                    log("Waiting " + getPingIntervalMilli() + " milliseconds before retrying ...");
                    Thread.sleep(getPingIntervalMilli());
                } else {
                    break;
                }
            }
        }
        
        // if 'socket' is still null then 'maxPings' has been exceeded
        if (socket == null) {
            throw new ConnectException("maxPings exceeded: " + getMaxPings() + ". Giving up. The clamd daemon seems not to be running");
        }
        
        try {
            // get the reader and writer to ping and receive pong
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ASCII"));
            PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
            
            log("Sending: \"PING\" to " + address + " ...");
            writer.println("PING");
            writer.flush();
            
            boolean pongReceived = false;
            for (;;) {
                String answer = reader.readLine();
                if (answer != null) {
                    answer = answer.trim();
                    log("Received: \"" + answer + "\"");
                    answer = answer.trim();
                    if (answer.equals("PONG")) {
                        pongReceived = true;
                    }
                    
                } else {
                    break;
                }
            }
            
            reader.close();
            writer.close();
            
            if (!pongReceived) {
                throw new ConnectException("Bad answer from \"PING\" probe: expecting \"PONG\"");
            }
        } finally {
            socket.close();
        }
    
protected final voidsaveChanges(javax.mail.internet.MimeMessage message)
Saves changes resetting the original message id.

param
message the message to save

        String messageId = message.getMessageID();
        message.saveChanges();
        if (messageId != null) {
            message.setHeader(RFC2822Headers.MESSAGE_ID, messageId);
        }
    
public voidservice(org.apache.mailet.Mail mail)
Scans the mail.

param
mail the mail to scan
throws
MessagingException if a problem arises

        
        // if already checked no action
        if (mail.getAttribute(MAIL_ATTRIBUTE_NAME) != null) {
            return;
        }
        
        MimeMessage mimeMessage = mail.getMessage();
        
        if (mimeMessage == null) {
            log("Null MimeMessage. Will send to ghost");
            // write mail info to log
            logMailInfo(mail);
            mail.setState(Mail.GHOST);
            return;
        }
        
        // get the socket
        Socket socket = getClamdSocket();
        BufferedReader reader = null;
        PrintWriter writer = null;
        Socket streamSocket = null;
        BufferedOutputStream bos = null;
            
        try {
            
            // prepare the reader and writer for the commands
            reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ASCII"));
            writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
            
            // write a request for a port to use for streaming out the data to scan
            writer.println("STREAM");
            writer.flush();
            
            // parse and get the "stream" port#
            int streamPort = getStreamPortFromAnswer(reader.readLine());
            
            // get the "stream" socket and the related (buffered) output stream
            streamSocket = new Socket(socket.getInetAddress(), streamPort);
            bos = new BufferedOutputStream(streamSocket.getOutputStream(), getStreamBufferSize());
            
            // stream out the message to the scanner
            mimeMessage.writeTo(bos);
            bos.flush();
            bos.close();
            streamSocket.close();
            
            String answer = null;
            boolean virusFound = false;
            String logMessage = "";
            for (;;) {
                answer = reader.readLine();
                if (answer != null) {
                    answer = answer.trim();
                    
                    // if a virus is found the answer will be '... FOUND'
                    if (answer.substring(answer.length() - FOUND_STRING.length()).equals(FOUND_STRING)) {
                        virusFound = true;
                        logMessage = answer + " (by CLAMD on " + socket.getInetAddress() + ")";
                        log(logMessage);
                    }
                } else {
                    break;
                }
            }
            
            reader.close();
            writer.close();
            
            if (virusFound) {
                String errorMessage = mail.getErrorMessage();
                if (errorMessage == null) {
                    errorMessage = "";
                } else {
                    errorMessage += "\r\n";
                }
                StringBuffer sb = new StringBuffer(errorMessage);
                sb.append(logMessage + "\r\n");
                
                // write mail and message info to log
                logMailInfo(mail);
                logMessageInfo(mimeMessage);
                
                // mark the mail with a mail attribute to check later on by other matchers/mailets
                mail.setAttribute(MAIL_ATTRIBUTE_NAME, "true");
                
                // sets the error message to be shown in any "notifyXxx" message
                mail.setErrorMessage(sb.toString());
                
                // mark the message with a header string
                mimeMessage.setHeader(HEADER_NAME, "true");
                
            } else {
                if (isDebug()) {
                    log("OK (by CLAMD on " + socket.getInetAddress() + ")");
                }
                mail.setAttribute(MAIL_ATTRIBUTE_NAME, "false");
                
                // mark the message with a header string
                mimeMessage.setHeader(HEADER_NAME, "false");
                
            }
            
            try {
                saveChanges(mimeMessage);
            } catch (Exception ex) {
                log("Exception caught while saving changes (header) to the MimeMessage. Ignoring ...", ex);
            }
            
        } catch (Exception ex) {
            log("Exception caught calling CLAMD on " + socket.getInetAddress() + ": " + ex.getMessage(), ex);
            throw new MessagingException("Exception caught", ex);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (Throwable t) {}
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (Throwable t) {}
            try {
                if (bos != null) {
                    bos.close();
                }
            } catch (Throwable t) {}
            try {
                if (streamSocket != null) {
                    streamSocket.close();
                }
            } catch (Throwable t) {}
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (Throwable t) {}
        }
        
    
protected voidsetAddresses(java.net.InetAddress[] addresses)
Setter for property addresses.

param
addresses New value of property addresses.


        this.addresses = addresses;
    
public voidsetDebug(boolean debug)
Setter for property debug.

param
debug New value of property debug.

        this.debug = debug;
    
public voidsetHost(java.lang.String host)
Setter for property host. Resolves also the host name into the corresponding IP addresses, issues a {@link #setAddresses} and resets the nextAddressIndex variable to 0 for dealing with round-robin.

param
host New value of property host.
throws
UnknownHostException if unable to resolve the host name, or if invalid

        
        this.host = host;
        
        setAddresses(InetAddress.getAllByName(host));
        
        nextAddressIndex = 0;
    
public voidsetMaxPings(int maxPings)
Setter for property maxPings.

param
maxPings New value of property maxPings.

        
        this.maxPings = maxPings;
    
public voidsetPingIntervalMilli(int pingIntervalMilli)
Setter for property pingIntervalMilli.

param
pingIntervalMilli New value of property pingIntervalMilli.

        
        this.pingIntervalMilli = pingIntervalMilli;
    
public voidsetPort(int port)
Setter for property port.

param
port New value of property port.

        
        this.port = port;
    
public voidsetStreamBufferSize(int streamBufferSize)
Setter for property streamBufferSize.

param
streamBufferSize New value of property streamBufferSize.

        
        this.streamBufferSize = streamBufferSize;