You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by Bill Champ <ja...@falcon.mychamp.com> on 2006/10/24 06:16:13 UTC

First post - a spamd mailet

Okay, this is my first post to this list. I looked through the archives
and did not see much discussion of spamd. I've been using it with great
success over the last few months on my james server. I'd like to
contribute the code to the project, but I'm unsure how to do that. The
guidelines links on the james page were broken.

Anyway, here's the code for anyone to use. You'll probably want to
change the package name. I use a separate package when I can to keep my
changes away from the official project code. Any suggestions will be welcome.

--Bill

/***********************************************************************
 * Copyright (c) 2006 The Apache Software Foundation.                  *
 * All rights reserved.                                                *
 * ------------------------------------------------------------------- *
 * Licensed under the Apache License, Version 2.0 (the "License"); you *
 * may not use this file except in compliance with the License. You    *
 * may obtain a copy of the License at:                                *
 *                                                                     *
 *     http://www.apache.org/licenses/LICENSE-2.0                      *
 *                                                                     *
 * Unless required by applicable law or agreed to in writing, software *
 * distributed under the License is distributed on an "AS IS" BASIS,   *
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
 * implied.  See the License for the specific language governing       *
 * permissions and limitations under the License.                      *
 * @author Bill Champ                                                  *
 ***********************************************************************/
package com.mychamp.mail.maillist;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import org.apache.james.core.MailImpl;
import org.apache.james.services.MailServer;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailetContext;
import org.apache.mailet.MailetException;

/**
 * A mailet for filtering mail through spamd, SpamAssassin's daemon.
 * Spamd must run on the same machine as this code because spamd will
 * only accept local connections.
 * 
 * The mailet tag in the config file might look like this:"

  <mailet match="All" class="Spamd">
      <spamd-host>127.0.0.1</spamd-host>
      <spamd-host-tag>falcon.local</spamd-host-tag>
      <port>783</port>
      <processor>transport</processor>
 </mailet>
 * @author Bill Champ
 *
 */
public class Spamd extends GenericMailet
{
    private String spamdHost = "localhost";
    private String spamdHostTag = "falcon.local";
    private int port = 783; // spamd's default port
    private String destination = "transport";

    public void init () throws MailetException
    {
        String portStr = this.getInitParameter ("port");
        if (portStr == null || portStr.length () == 0)
        {
            throw new MailetException ("port parameter is required");
        }
        try
        {
            this.port = Integer.parseInt (portStr);
        }
        catch (NumberFormatException nfe)
        {
            throw new MailetException ("port parameter does not make
sense: " + portStr);
        }
        
        String hostStr = this.getInitParameter ("spamd-host");
        if (hostStr == null || hostStr.length () == 0)
        {
            throw new MailetException ("spamd-host parameter is required");
        }
        this.spamdHost = hostStr;
        
        String hostTagStr = this.getInitParameter ("spamd-host-tag");
        if (hostTagStr == null || hostTagStr.length () == 0)
        {
            throw new MailetException ("spamd-host-tag parameter is
required");
        }
        this.spamdHostTag = hostTagStr;
        
        this.destination = this.getInitParameter ("processor");
    } // init ()

    public void service (Mail mail) throws MessagingException
    {
        // First check to see if we have already run this message
through spamd.
        // Otherwise the new message created below will also be checked
and so on --
        // endless recursion.
        String[] header = mail.getMessage().getHeader ("X-Spam-Checker-
Version");
        if (header != null && header.length > 0)
        {
            int pos = header[0].indexOf ("SpamAssassin");
            boolean saEnding = header[0].endsWith ("on " + this.spamdHostTag);
            if (pos != -1 && saEnding == true)
            {
                return; // already checked it for spam.
            }
        }
        if (mail instanceof MailImpl) // need the message size
        {
            try
            {
                long size = ((MailImpl) mail).getMessageSize ();
                MailetContext context = this.getMailetContext ();
                MailServer jamesServer = null;
                
                if (context instanceof MailServer)
                {
                    jamesServer = (MailServer) context;
                }
                
                InputStream is = converseWithSpamd (mail.getMessage (), size);
                if (is != null)
                {
                    Mail new_mail = new MailImpl (  jamesServer.getId (),
                                                    mail.getSender (), 
                                                    mail.getRecipients (),
                                                    is);
                    is.close();
                    
                    //send new message to destination processor
                    this.getMailetContext ().sendMail (new_mail.getSender (),
                            new_mail.getRecipients (),
new_mail.getMessage (), this.destination);
                    mail.setState (Mail.GHOST); // kill orginal message
                }
            }
            catch (IOException ioe)
            {
                getMailetContext ().log ("IOException encountered when
talking to spamd.", ioe);
                //throw new MessagingException ("IOException encountered
when talking to spamd.", ioe);
            }
        } // if instanceof MailImpl
    } // service
    
    /**
     * Communicates with spamd to filter a mail message for spam content.
     * Used only by the main() method in this class for testing.
     * @param src An InputStrem to the mail message. Must include the headers
     * @param messageSize Size of mail message including the headers
     * @param dst OutputStream to capture spamd's response, which is a
transformation
     * of the original message with spamd's headers added. Spamd may
also change the 
     * subject line by adding a [SPAM] prefix.
     * @throws MessagingException
     */
    private void converseWithSpamd (    InputStream src, 
                                        long messageSize, 
                                        OutputStream dst) throws
MessagingException
    {
        try
        {
            InputStream message_is = src;
            OutputStream message_os = dst;
            Socket sock = new Socket (this.spamdHost, this.port);
            OutputStream os = sock.getOutputStream ();
            InputStream is = sock.getInputStream ();

            // send the headers required by the protocol
            String line = "PROCESS SPAMC/1.2\r\n";
            os.write (line.getBytes ("US-ASCII"));
            line = "Content-length: " + (messageSize + 4) + "\r\n\r\n";
            os.write (line.getBytes ("US-ASCII"));

            // now send the message
            byte[] buffer = new byte[50 * 1024]; // 50K at a time
            int bytesRead = message_is.read (buffer);
            while (bytesRead != -1)
            {
                os.write (buffer, 0, bytesRead);
                bytesRead = message_is.read (buffer);
            }
            line = "\r\n\r\n"; // insure there is a blank line at end
            os.write (line.getBytes ("US-ASCII"));
            os.flush ();

            // os.close (); // Closes the socket, not just the
OutputStream. Better wait.
            message_is.close ();

            // Now let's start looking at the response.
            skipPastBlankLine (is); // skip spamd's response header and
get to the message itself

            // write message back through the DataHandler. This
overwrites existing message.
            bytesRead = is.read (buffer);
            while (bytesRead != -1)
            {
                message_os.write (buffer, 0, bytesRead);
                bytesRead = is.read (buffer);
            }
            is.close ();
            os.close ();
            message_os.flush ();
            message_os.close ();
        }
        catch (IOException ioe)
        {
            throw new MessagingException ("IOException encountered when
talking to spamd.", ioe);
        }
    } // converseWithSpamd ()
    
    /**
     * Communicates with spamd to filter a mail message for spam content.
     * @param msg the MimeMessage to filter
     * @param messageSize number of bytes in message, including headers
     * @return The InputStream coming from spamd which contains the
altered message.
     * @throws MessagingException
     */
    private InputStream converseWithSpamd (MimeMessage msg, long
messageSize) throws MessagingException
    {
        InputStream is = null;
        try
        {
            Socket sock = new Socket (this.spamdHost, this.port);
            OutputStream os = sock.getOutputStream ();
            is = sock.getInputStream ();

            // send the headers required by the protocol
            String line = "PROCESS SPAMC/1.2\r\n";
            os.write (line.getBytes ("US-ASCII"));
            line = "Content-length: " + (messageSize + 4) + "\r\n\r\n";
            os.write (line.getBytes ("US-ASCII"));
            
            // now send the message
            msg.writeTo (os);

            line = "\r\n\r\n"; // insure there is a blank line at end
            os.write (line.getBytes ("US-ASCII"));
            os.flush ();

            // Now let's start looking at the response.
            skipPastBlankLine (is); // skip spamd's response header and
get to the message itself
        }
        catch (IOException ioe)
        {
            throw new MessagingException ("IOException encountered when
talking to spamd.", ioe);
        }
        return is;
    } // converseWithSpamd ()
    

    

    /**
     * Run the stream forward until we get just past a blank 
     * line defined as CR LF CR LF ("\r\n\r\n").
     * 
     * @param is The stream
     * @throws IOException
     */
    private void skipPastBlankLine (InputStream is) throws IOException
    {
        final int CR = 13;
        final int LF = 10;
        int stage = 0;
        do
        {
            int value = is.read ();
            switch (stage)
            {
                case 0:
                case 2:
                {
                    if (value == CR)
                    {
                        stage++;
                    }
                    else
                    {
                        stage = 0;
                    }
                    break;
                }
                case 1:
                case 3:
                {
                    if (value == LF)
                    {
                        stage++;
                    }
                    else
                    {
                        stage = 0;
                    }
                    break;
                }
            } // switch
        } while (stage < 4);
    } // skipPastBlankLine ()
    
    /**
     * Return a string describing this mailet.
     * 
     * @return a string describing this mailet
     */
    public String getMailetInfo ()
    {
        return "Spamd Mailet";
    }
    
    /**
     * main method for testing.
     */
    public static void main (String[] args)
    {
        try
        {
            String inputFileName = "/Users/bchamp/Desktop/
testMsg.FileStreamStore";
            String outputFileName = "/Users/bchamp/Desktop/sa.out";
            File f = new File (inputFileName);
            FileInputStream fis = new FileInputStream (f);
            FileOutputStream fos = new FileOutputStream (outputFileName);
            
            Spamd app = new Spamd ();
            app.converseWithSpamd (fis, f.length (), fos);
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace ();
        }
        catch (MessagingException me)
        {
            me.printStackTrace ();
        }

    } // main ()

} // class Spamd



---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org


Re: First post - a spamd mailet

Posted by Stefano Bagnara <ap...@bago.org>.
Can you check this url?
https://issues.apache.org/jira/browse/JAMES-259

And this mailet currently included in our trunk:
http://svn.apache.org/viewvc/james/server/trunk/src/java/org/apache/james/transport/mailets/SpamAssassin.java?view=markup

if you think your version is better in any way please attach it to the 
issue above granting ASF inclusion and explaining why you think this is 
better.

Stefano

Bill Champ wrote:
> Okay, this is my first post to this list. I looked through the archives
> and did not see much discussion of spamd. I've been using it with great
> success over the last few months on my james server. I'd like to
> contribute the code to the project, but I'm unsure how to do that. The
> guidelines links on the james page were broken.
> 
> Anyway, here's the code for anyone to use. You'll probably want to
> change the package name. I use a separate package when I can to keep my
> changes away from the official project code. Any suggestions will be welcome.
> 
> --Bill
> 
> /***********************************************************************
>  * Copyright (c) 2006 The Apache Software Foundation.                  *
>  * All rights reserved.                                                *
>  * ------------------------------------------------------------------- *
>  * Licensed under the Apache License, Version 2.0 (the "License"); you *
>  * may not use this file except in compliance with the License. You    *
>  * may obtain a copy of the License at:                                *
>  *                                                                     *
>  *     http://www.apache.org/licenses/LICENSE-2.0                      *
>  *                                                                     *
>  * Unless required by applicable law or agreed to in writing, software *
>  * distributed under the License is distributed on an "AS IS" BASIS,   *
>  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
>  * implied.  See the License for the specific language governing       *
>  * permissions and limitations under the License.                      *
>  * @author Bill Champ                                                  *
>  ***********************************************************************/
> package com.mychamp.mail.maillist;
> 
> import java.io.File;
> import java.io.FileInputStream;
> import java.io.FileOutputStream;
> import java.io.IOException;
> import java.io.InputStream;
> import java.io.OutputStream;
> import java.net.Socket;
> 
> import javax.mail.MessagingException;
> import javax.mail.internet.MimeMessage;
> 
> import org.apache.james.core.MailImpl;
> import org.apache.james.services.MailServer;
> import org.apache.mailet.GenericMailet;
> import org.apache.mailet.Mail;
> import org.apache.mailet.MailetContext;
> import org.apache.mailet.MailetException;
> 
> /**
>  * A mailet for filtering mail through spamd, SpamAssassin's daemon.
>  * Spamd must run on the same machine as this code because spamd will
>  * only accept local connections.
>  * 
>  * The mailet tag in the config file might look like this:"
> 
>   <mailet match="All" class="Spamd">
>       <spamd-host>127.0.0.1</spamd-host>
>       <spamd-host-tag>falcon.local</spamd-host-tag>
>       <port>783</port>
>       <processor>transport</processor>
>  </mailet>
>  * @author Bill Champ
>  *
>  */
> public class Spamd extends GenericMailet
> {
>     private String spamdHost = "localhost";
>     private String spamdHostTag = "falcon.local";
>     private int port = 783; // spamd's default port
>     private String destination = "transport";
> 
>     public void init () throws MailetException
>     {
>         String portStr = this.getInitParameter ("port");
>         if (portStr == null || portStr.length () == 0)
>         {
>             throw new MailetException ("port parameter is required");
>         }
>         try
>         {
>             this.port = Integer.parseInt (portStr);
>         }
>         catch (NumberFormatException nfe)
>         {
>             throw new MailetException ("port parameter does not make
> sense: " + portStr);
>         }
>         
>         String hostStr = this.getInitParameter ("spamd-host");
>         if (hostStr == null || hostStr.length () == 0)
>         {
>             throw new MailetException ("spamd-host parameter is required");
>         }
>         this.spamdHost = hostStr;
>         
>         String hostTagStr = this.getInitParameter ("spamd-host-tag");
>         if (hostTagStr == null || hostTagStr.length () == 0)
>         {
>             throw new MailetException ("spamd-host-tag parameter is
> required");
>         }
>         this.spamdHostTag = hostTagStr;
>         
>         this.destination = this.getInitParameter ("processor");
>     } // init ()
> 
>     public void service (Mail mail) throws MessagingException
>     {
>         // First check to see if we have already run this message
> through spamd.
>         // Otherwise the new message created below will also be checked
> and so on --
>         // endless recursion.
>         String[] header = mail.getMessage().getHeader ("X-Spam-Checker-
> Version");
>         if (header != null && header.length > 0)
>         {
>             int pos = header[0].indexOf ("SpamAssassin");
>             boolean saEnding = header[0].endsWith ("on " + this.spamdHostTag);
>             if (pos != -1 && saEnding == true)
>             {
>                 return; // already checked it for spam.
>             }
>         }
>         if (mail instanceof MailImpl) // need the message size
>         {
>             try
>             {
>                 long size = ((MailImpl) mail).getMessageSize ();
>                 MailetContext context = this.getMailetContext ();
>                 MailServer jamesServer = null;
>                 
>                 if (context instanceof MailServer)
>                 {
>                     jamesServer = (MailServer) context;
>                 }
>                 
>                 InputStream is = converseWithSpamd (mail.getMessage (), size);
>                 if (is != null)
>                 {
>                     Mail new_mail = new MailImpl (  jamesServer.getId (),
>                                                     mail.getSender (), 
>                                                     mail.getRecipients (),
>                                                     is);
>                     is.close();
>                     
>                     //send new message to destination processor
>                     this.getMailetContext ().sendMail (new_mail.getSender (),
>                             new_mail.getRecipients (),
> new_mail.getMessage (), this.destination);
>                     mail.setState (Mail.GHOST); // kill orginal message
>                 }
>             }
>             catch (IOException ioe)
>             {
>                 getMailetContext ().log ("IOException encountered when
> talking to spamd.", ioe);
>                 //throw new MessagingException ("IOException encountered
> when talking to spamd.", ioe);
>             }
>         } // if instanceof MailImpl
>     } // service
>     
>     /**
>      * Communicates with spamd to filter a mail message for spam content.
>      * Used only by the main() method in this class for testing.
>      * @param src An InputStrem to the mail message. Must include the headers
>      * @param messageSize Size of mail message including the headers
>      * @param dst OutputStream to capture spamd's response, which is a
> transformation
>      * of the original message with spamd's headers added. Spamd may
> also change the 
>      * subject line by adding a [SPAM] prefix.
>      * @throws MessagingException
>      */
>     private void converseWithSpamd (    InputStream src, 
>                                         long messageSize, 
>                                         OutputStream dst) throws
> MessagingException
>     {
>         try
>         {
>             InputStream message_is = src;
>             OutputStream message_os = dst;
>             Socket sock = new Socket (this.spamdHost, this.port);
>             OutputStream os = sock.getOutputStream ();
>             InputStream is = sock.getInputStream ();
> 
>             // send the headers required by the protocol
>             String line = "PROCESS SPAMC/1.2\r\n";
>             os.write (line.getBytes ("US-ASCII"));
>             line = "Content-length: " + (messageSize + 4) + "\r\n\r\n";
>             os.write (line.getBytes ("US-ASCII"));
> 
>             // now send the message
>             byte[] buffer = new byte[50 * 1024]; // 50K at a time
>             int bytesRead = message_is.read (buffer);
>             while (bytesRead != -1)
>             {
>                 os.write (buffer, 0, bytesRead);
>                 bytesRead = message_is.read (buffer);
>             }
>             line = "\r\n\r\n"; // insure there is a blank line at end
>             os.write (line.getBytes ("US-ASCII"));
>             os.flush ();
> 
>             // os.close (); // Closes the socket, not just the
> OutputStream. Better wait.
>             message_is.close ();
> 
>             // Now let's start looking at the response.
>             skipPastBlankLine (is); // skip spamd's response header and
> get to the message itself
> 
>             // write message back through the DataHandler. This
> overwrites existing message.
>             bytesRead = is.read (buffer);
>             while (bytesRead != -1)
>             {
>                 message_os.write (buffer, 0, bytesRead);
>                 bytesRead = is.read (buffer);
>             }
>             is.close ();
>             os.close ();
>             message_os.flush ();
>             message_os.close ();
>         }
>         catch (IOException ioe)
>         {
>             throw new MessagingException ("IOException encountered when
> talking to spamd.", ioe);
>         }
>     } // converseWithSpamd ()
>     
>     /**
>      * Communicates with spamd to filter a mail message for spam content.
>      * @param msg the MimeMessage to filter
>      * @param messageSize number of bytes in message, including headers
>      * @return The InputStream coming from spamd which contains the
> altered message.
>      * @throws MessagingException
>      */
>     private InputStream converseWithSpamd (MimeMessage msg, long
> messageSize) throws MessagingException
>     {
>         InputStream is = null;
>         try
>         {
>             Socket sock = new Socket (this.spamdHost, this.port);
>             OutputStream os = sock.getOutputStream ();
>             is = sock.getInputStream ();
> 
>             // send the headers required by the protocol
>             String line = "PROCESS SPAMC/1.2\r\n";
>             os.write (line.getBytes ("US-ASCII"));
>             line = "Content-length: " + (messageSize + 4) + "\r\n\r\n";
>             os.write (line.getBytes ("US-ASCII"));
>             
>             // now send the message
>             msg.writeTo (os);
> 
>             line = "\r\n\r\n"; // insure there is a blank line at end
>             os.write (line.getBytes ("US-ASCII"));
>             os.flush ();
> 
>             // Now let's start looking at the response.
>             skipPastBlankLine (is); // skip spamd's response header and
> get to the message itself
>         }
>         catch (IOException ioe)
>         {
>             throw new MessagingException ("IOException encountered when
> talking to spamd.", ioe);
>         }
>         return is;
>     } // converseWithSpamd ()
>     
> 
>     
> 
>     /**
>      * Run the stream forward until we get just past a blank 
>      * line defined as CR LF CR LF ("\r\n\r\n").
>      * 
>      * @param is The stream
>      * @throws IOException
>      */
>     private void skipPastBlankLine (InputStream is) throws IOException
>     {
>         final int CR = 13;
>         final int LF = 10;
>         int stage = 0;
>         do
>         {
>             int value = is.read ();
>             switch (stage)
>             {
>                 case 0:
>                 case 2:
>                 {
>                     if (value == CR)
>                     {
>                         stage++;
>                     }
>                     else
>                     {
>                         stage = 0;
>                     }
>                     break;
>                 }
>                 case 1:
>                 case 3:
>                 {
>                     if (value == LF)
>                     {
>                         stage++;
>                     }
>                     else
>                     {
>                         stage = 0;
>                     }
>                     break;
>                 }
>             } // switch
>         } while (stage < 4);
>     } // skipPastBlankLine ()
>     
>     /**
>      * Return a string describing this mailet.
>      * 
>      * @return a string describing this mailet
>      */
>     public String getMailetInfo ()
>     {
>         return "Spamd Mailet";
>     }
>     
>     /**
>      * main method for testing.
>      */
>     public static void main (String[] args)
>     {
>         try
>         {
>             String inputFileName = "/Users/bchamp/Desktop/
> testMsg.FileStreamStore";
>             String outputFileName = "/Users/bchamp/Desktop/sa.out";
>             File f = new File (inputFileName);
>             FileInputStream fis = new FileInputStream (f);
>             FileOutputStream fos = new FileOutputStream (outputFileName);
>             
>             Spamd app = new Spamd ();
>             app.converseWithSpamd (fis, f.length (), fos);
>         }
>         catch (IOException ioe)
>         {
>             ioe.printStackTrace ();
>         }
>         catch (MessagingException me)
>         {
>             me.printStackTrace ();
>         }
> 
>     } // main ()
> 
> } // class Spamd



---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org