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

DSNBounce

public class DSNBounce extends AbstractNotify

Generates a Delivery Status Notification (DSN) Note that this is different than a mail-client's reply, which would use the Reply-To or From header.

Bounced messages are attached in their entirety (headers and content) and the resulting MIME part type is "message/rfc822".
The reverse-path and the Return-Path header of the response is set to "null" ("<>"), meaning that no reply should be sent.

A sender of the notification message can optionally be specified. If one is not specified, the postmaster's address will be used.

Supports the passThrough init parameter (true if missing).

Sample configuration:


<mailet match="All" class="DSNBounce">
<sender>an address or postmaster or sender or unaltered,
default=postmaster</sender>
<prefix>optional subject prefix prepended to the original
message</prefix>
<attachment>message, heads or none, default=message</attachment>
<messageString>the message sent in the bounce, the first occurrence of the pattern [machine] is replaced with the name of the executing machine, default=Hi. This is the James mail server at [machine] ... </messageString>
<passThrough>true or false, default=true</passThrough>
<debug>true or false, default=false</debug>
</mailet>
see
org.apache.james.transport.mailets.AbstractNotify

Fields Summary
private static final org.apache.mailet.dates.RFC822DateFormat
rfc822DateFormat
private static final Random
random
private static org.apache.oro.text.regex.Pattern
statusPattern
private static org.apache.oro.text.regex.Pattern
diagPattern
private static final String
MACHINE_PATTERN
private String
messageString
Constructors Summary
Methods Summary
private 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 javax.mail.internet.MimeBodyPartcreateAttachedOriginal(org.apache.mailet.Mail originalMail, int attachmentType)
Create a MimeBodyPart with the original Mail as Attachment

param
originalMail
return
MimeBodyPart
throws
MessagingException

        MimeBodyPart part = new MimeBodyPart();
        MimeMessage originalMessage = originalMail.getMessage();
        
        if (attachmentType == HEADS) {
            part.setContent(getMessageHeaders(originalMessage), "text/plain");
            part.setHeader("Content-Type","text/rfc822-headers");
        } else {
            part.setContent(originalMessage, "message/rfc822");
        }
        
        if ((originalMessage.getSubject() != null) && 
            (originalMessage.getSubject().trim().length() > 0)) {
            part.setFileName(originalMessage.getSubject().trim());
        } else {
            part.setFileName("No Subject");
        }
        part.setDisposition("Attachment");
        return part;
    
protected javax.mail.internet.MimeBodyPartcreateDSN(org.apache.mailet.Mail originalMail)
creates the DSN-bodypart for automated processing

param
originalMail
return
MimeBodyPart dsn-bodypart
throws
MessagingException

        MimeBodyPart dsn = new MimeBodyPart();
        StringWriter sout = new StringWriter();
        PrintWriter out = new PrintWriter(sout, true);
        String nameType = null;


        ////////////////////////
        // per message fields //
        ////////////////////////

        //optional: envelope-id
        // TODO: Envelope-Id
        // The Original-Envelope-Id is NOT the same as the Message-Id from the header.
        // The Message-Id identifies the content of the message, while the Original-Envelope-ID
        // identifies the transaction in which the message is sent.  (see RFC3461)
        // so do NOT out.println("Original-Envelope-Id:"+originalMail.getMessage().getMessageID());


        //required: reporting MTA
        // this is always us, since we do not translate non-internet-mail
        // failure reports into DSNs
        nameType = "dns";
        try {
            String myAddress =
                (String)getMailetContext().getAttribute(Constants.HELLO_NAME);
            /*
            String myAddress = InetAddress.getLocalHost().getCanonicalHostName();
            */
            out.println("Reporting-MTA: "+nameType+"; "+myAddress);
        } catch(Exception e){
            // we should always know our address, so we shouldn't get here
            log("WARNING: sending DSN without required Reporting-MTA Address");
        }

        //only for gateways to non-internet mail systems: dsn-gateway

        //optional: received from
        out.println("Received-From-MTA: "+nameType+"; "+originalMail.getRemoteHost());

        //optional: Arrival-Date

        //////////////////////////
        // per recipient fields //
        //////////////////////////

        Iterator recipients = originalMail.getRecipients().iterator();
        while (recipients.hasNext())
            {
                MailAddress rec = (MailAddress)recipients.next();
                String addressType = "rfc822";

                //required: blank line
                out.println();

                //optional: original recipient (see RFC3461)
                //out.println("Original-Recipient: "+addressType+"; "+ ??? );

                //required: final recipient
                out.println("Final-Recipient: "+addressType+"; "+rec.toString());

                //required: action
                // alowed values: failed, delayed, delivered, relayed, expanded
                // TODO: until now, we do error-bounces only
                out.println("Action: failed");

                //required: status
                // get Exception for getting status information
                // TODO: it would be nice if the SMTP-handler would set a status attribute we can use here
                MessagingException ex =
                    (MessagingException) originalMail.getAttribute("delivery-error");
                out.println("Status: "+getStatus(ex));

                //optional: remote MTA
                //to which MTA were we talking while the Error occured?

                //optional: diagnostic-code
                String diagnosticType = null;
                // this typically is the return value received during smtp
                // (or other transport) communication
                // and should be stored as attribute by the smtp handler
                // but until now we only have error-messages.
                String diagnosticCode = getErrorMsg(ex);
                // Sometimes this is the smtp diagnostic code,
                // but James often gives us other messages
                Perl5Matcher diagMatcher = new Perl5Matcher();
                boolean smtpDiagCodeAvailable =
                    diagMatcher.matches(diagnosticCode, diagPattern);
                if (smtpDiagCodeAvailable){
                    diagnosticType = "smtp";
                } else {
                    diagnosticType = "X-James";
                }
                out.println("Diagnostic-Code: "+diagnosticType+"; "+diagnosticCode);
            
                //optional: last attempt
                out.println("Last-Attempt-Date: "+
                            rfc822DateFormat.format(originalMail.getLastUpdated()));

                //optional: retry until
                //only for 'delayed' reports .. but we don't report this (at least until now)

                //optional: extension fields

            }


        // Changed this from rfc822 handling to text/plain
        // It should be handled correctly as delivery-status but it
        // is better text/plain than rfc822 (rfc822 add message headers not
        // allowed in the delivery-status.
        // text/plain does not support rfc822 header encoding that we
        // should support here.
        dsn.setContent(sout.toString(), "text/plain");
        dsn.setHeader("Content-Type","message/delivery-status");
        dsn.setDescription("Delivery Status Notification");
        dsn.setFileName("status.dat");
        return dsn;
    
protected javax.mail.internet.MimeBodyPartcreateTextMsg(org.apache.mailet.Mail originalMail)
Create a MimeBodyPart with a textual description for human readers.

param
originalMail
return
MimeBodyPart
throws
MessagingException

        MimeBodyPart part1 = new MimeBodyPart();
        StringWriter sout = new StringWriter();
        PrintWriter out = new PrintWriter(sout, true);
        String machine = "[unknown]";
        try {
            InetAddress me = InetAddress.getLocalHost();
            machine = me.getHostName();
        } catch(Exception e){
            machine = "[address unknown]";
        }

        StringBuffer bounceBuffer =
            new StringBuffer(128).append (messageString);
        int m_idx_begin = messageString.indexOf(MACHINE_PATTERN);
        if (m_idx_begin != -1) {
            bounceBuffer.replace (m_idx_begin,
                                  m_idx_begin+MACHINE_PATTERN.length(),
                                  machine);
        }
        out.println(bounceBuffer.toString());
        out.println("Failed recipient(s):");
        for (Iterator i = originalMail.getRecipients().iterator(); i.hasNext(); ) {
            out.println(i.next());
        }
        MessagingException ex = (MessagingException)originalMail.getAttribute("delivery-error");
        out.println();
        out.println("Error message:");
        out.println(getErrorMsg(ex));
        out.println();

        part1.setText(sout.toString());
        return part1;
    
protected java.lang.String[]getAllowedInitParameters()
Gets the expected init parameters.

        String[] allowedArray = {
            "debug",
            "passThrough",
            "messageString",
            "attachment",
            "sender",
            "prefix"
        };
        return allowedArray;
    
protected intgetAttachmentType()

return
the attachment init parameter, or MESSAGE if missing

        return getTypeCode(getInitParameter("attachment","message"));
    
protected java.lang.StringgetErrorMsg(javax.mail.MessagingException me)
Utility method for getting the error message from the (nested) exception.

param
MessagingException
return
error message

        if (me.getNextException() == null) {
            return me.getMessage().trim();
        } else {
            Exception ex1 = me.getNextException();
            return ex1.getMessage().trim();
        }
    
public java.lang.StringgetMailetInfo()

        return "DSNBounce Mailet";
    
protected java.util.CollectiongetRecipients()

return
SpecialAddress.REVERSE_PATH

        Collection newRecipients = new HashSet();
        newRecipients.add(SpecialAddress.REVERSE_PATH);
        return newRecipients;
    
protected org.apache.mailet.MailAddressgetReversePath(org.apache.mailet.Mail originalMail)

return
SpecialAddress.NULL (the meaning of bounce)

        return SpecialAddress.NULL;
    
protected java.lang.StringgetStatus(javax.mail.MessagingException me)
Guessing status code by the exception provided. This method should use the status attribute when the SMTP-handler somewhen provides it

param
MessagingException
return
status code

        if (me.getNextException() == null) {
            String mess = me.getMessage();
            Perl5Matcher m = new Perl5Matcher();
            StringBuffer sb = new StringBuffer();
            if (m.matches(mess, statusPattern)) {
                MatchResult res = m.getMatch();
                sb.append(res.group(1));
                return sb.toString();
            }
            // bad destination system adress
            if (mess.startsWith("There are no DNS entries for the hostname"))
                return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.ADDRESS_SYSTEM);

            // no answer from host (4.4.1) or
            // system not accepting network messages (4.3.2), lets guess ...
            if (mess.equals("No mail server(s) available at this time."))
                return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_NO_ANSWER);

            // other/unknown error
            return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.UNDEFINED_STATUS);
        } else {
            Exception ex1 = me.getNextException();
            Perl5Matcher m = new Perl5Matcher ();
            StringBuffer sb = new StringBuffer();
            if (m.matches(ex1.getMessage(), statusPattern)) {
                MatchResult res = m.getMatch();
                sb.append(res.group(1));
                return sb.toString();
            } else if (ex1 instanceof SendFailedException) {
                // other/undefined protocol status
                int smtpCode = 0;
                try {
                    smtpCode = Integer.parseInt(ex1.getMessage().substring(0,3));
                } catch(NumberFormatException e) {
                }

                switch (smtpCode) {
                
                    // Req mail action not taken: mailbox unavailable
                    case 450: return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.MAILBOX_OTHER);
                    // Req action aborted: local error in processing
                    case 451: return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.SYSTEM_OTHER);
                    // Req action not taken: insufficient sys storage
                    case 452: return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.SYSTEM_FULL);
                    // Syntax error, command unrecognized
                    case 500: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_SYNTAX);
                    // Syntax error in parameters or arguments
                    case 501: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_ARG);
                    // Command not implemented
                    case 502: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_CMD);
                    // Bad sequence of commands
                    case 503: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_CMD);
                    // Command parameter not implemented
                    case 504: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_ARG);
                    // Req mail action not taken: mailbox unavailable
                    case 550: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.MAILBOX_OTHER);
                    // User not local; please try <...>
                    // 5.7.1 Select another host to act as your forwarder
                    case 551: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.SECURITY_AUTH);
                    // Req mail action aborted: exceeded storage alloc
                    case 552: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.MAILBOX_FULL);
                    // Req action not taken: mailbox name not allowed
                    case 553: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.ADDRESS_SYNTAX);
                    // Transaction failed
                    case 554: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.UNDEFINED_STATUS);
                    // Not authorized. This is not an SMTP code, but many server use it.
                    case 571: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.SECURITY_AUTH);
                    
                    default:
                        // if we get an smtp returncode starting with 4
                        // it is an persistent transient error, else permanent
                        if (ex1.getMessage().startsWith("4")) {
                            return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.DELIVERY_OTHER);
                        } else return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_OTHER);
                }

            } else if (ex1 instanceof UnknownHostException) {
                // bad destination system address
                return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.ADDRESS_SYSTEM);
            } else if (ex1 instanceof ConnectException) {
                // bad connection
                return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_CONNECTION);
            } else if (ex1 instanceof SocketException) {
                // bad connection
                return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_CONNECTION);
            } else {
                // other/undefined/unknown error
                return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.UNDEFINED_STATUS);
            }
        }
    
protected javax.mail.internet.InternetAddress[]getTo()

return
SpecialAddress.REVERSE_PATH

        InternetAddress[] apparentlyTo = new InternetAddress[1];
        apparentlyTo[0] = SpecialAddress.REVERSE_PATH.toInternetAddress();
        return apparentlyTo;
    
public voidinit()
Initialize the mailet


    /*
     * Static initializer.<p>
     * Compiles patterns for processing exception messages.<p>
     */
     
        Perl5Compiler compiler = new Perl5Compiler();
        String status_pattern_string = ".*\\s*([245]\\.\\d{1,3}\\.\\d{1,3}).*\\s*";
        String diag_pattern_string = "^\\d{3}\\s.*$";
        try {
            statusPattern = compiler.
                compile(status_pattern_string, Perl5Compiler.READ_ONLY_MASK);
        } catch(MalformedPatternException mpe) {
            //this should not happen as the pattern string is hardcoded.
            System.err.println ("Malformed pattern: " + status_pattern_string);
            mpe.printStackTrace (System.err);
        }
        try {
            diagPattern = compiler.
                compile(diag_pattern_string, Perl5Compiler.READ_ONLY_MASK);
        } catch(MalformedPatternException mpe) {
            //this should not happen as the pattern string is hardcoded.
            System.err.println ("Malformed pattern: " + diag_pattern_string);
        }
    
        super.init();
        messageString = getInitParameter("messageString","Hi. This is the James mail server at [machine].\nI'm afraid I wasn't able to deliver your message to the following addresses.\nThis is a permanent error; I've given up. Sorry it didn't work out.  Below\nI include the list of recipients and the reason why I was unable to deliver\nyour message.\n");
    
protected java.lang.StringnewName(org.apache.mailet.Mail mail)
Create a unique new primary key name.

param
mail the mail to use as the basis for the new mail name
return
a new name

        String oldName = mail.getName();

        // Checking if the original mail name is too long, perhaps because of a
        // loop caused by a configuration error.
        // it could cause a "null pointer exception" in AvalonMailRepository much
        // harder to understand.
        if (oldName.length() > 76) {
            int count = 0;
            int index = 0;
            while ((index = oldName.indexOf('!", index + 1)) >= 0) {
                count++;
            }
            // It looks like a configuration loop. It's better to stop.
            if (count > 7) {
                throw new MessagingException("Unable to create a new message name: too long."
                                             + " Possible loop in config.xml.");
            }
            else {
                oldName = oldName.substring(0, 76);
            }
        }

        StringBuffer nameBuffer =
            new StringBuffer(64)
            .append(oldName)
            .append("-!")
            .append(random.nextInt(1048576));
        return nameBuffer.toString();
    
public voidservice(org.apache.mailet.Mail originalMail)
Service does the hard work and bounces the originalMail in the format specified by RFC3464.

param
originalMail the mail to bounce
throws
MessagingException if a problem arises formulating the redirected mail
see
org.apache.mailet.Mailet#service(org.apache.mailet.Mail)



        // duplicates the Mail object, to be able to modify the new mail keeping the original untouched
        MailImpl newMail = new MailImpl(originalMail,newName(originalMail));
        try {
            // We don't need to use the original Remote Address and Host,
            // and doing so would likely cause a loop with spam detecting
            // matchers.
            try {
                newMail.setRemoteAddr(java.net.InetAddress.getLocalHost().getHostAddress());
                newMail.setRemoteHost(java.net.InetAddress.getLocalHost().getHostName());
            } catch (java.net.UnknownHostException _) {
                newMail.setRemoteAddr("127.0.0.1");
                newMail.setRemoteHost("localhost");
            }
    
            if (originalMail.getSender() == null) {
                if (isDebug)
                    log("Processing a bounce request for a message with an empty reverse-path.  No bounce will be sent.");
                if(!getPassThrough(originalMail)) {
                    originalMail.setState(Mail.GHOST);
                }
                return;
            }
    
            MailAddress reversePath = originalMail.getSender();
            if (isDebug)
                log("Processing a bounce request for a message with a reverse path.  The bounce will be sent to " + reversePath);
    
            Collection newRecipients = new HashSet();
            newRecipients.add(reversePath);
            newMail.setRecipients(newRecipients);
    
            if (isDebug) {
                log("New mail - sender: " + newMail.getSender()
                    + ", recipients: " +
                    arrayToString(newMail.getRecipients().toArray())
                    + ", name: " + newMail.getName()
                    + ", remoteHost: " + newMail.getRemoteHost()
                    + ", remoteAddr: " + newMail.getRemoteAddr()
                    + ", state: " + newMail.getState()
                    + ", lastUpdated: " + newMail.getLastUpdated()
                    + ", errorMessage: " + newMail.getErrorMessage());
            }
    
            // create the bounce message
            MimeMessage newMessage =
                new MimeMessage(Session.getDefaultInstance(System.getProperties(),
                                                           null));
    
            MimeMultipartReport multipart = new MimeMultipartReport ();
            multipart.setReportType ("delivery-status");
            
            // part 1: descripive text message
            MimeBodyPart part1 = createTextMsg(originalMail);
            multipart.addBodyPart(part1);
    
            // part 2: DSN
            MimeBodyPart part2 = createDSN(originalMail);
            multipart.addBodyPart(part2);
    
    
            // part 3: original mail (optional)
            if (getAttachmentType() != NONE) {
                MimeBodyPart part3 = createAttachedOriginal(originalMail,getAttachmentType());
                multipart.addBodyPart(part3);
            }
    
    
            // stuffing all together
            newMessage.setContent(multipart);
            newMessage.setHeader(RFC2822Headers.CONTENT_TYPE, multipart.getContentType());
            newMail.setMessage(newMessage);
    
            //Set additional headers
            setRecipients(newMail, getRecipients(originalMail), originalMail);
            setTo(newMail, getTo(originalMail), originalMail);
            setSubjectPrefix(newMail, getSubjectPrefix(originalMail), originalMail);
            if(newMail.getMessage().getHeader(RFC2822Headers.DATE) == null) {
                newMail.getMessage().setHeader(RFC2822Headers.DATE,rfc822DateFormat.format(new Date()));
            }
            setReplyTo(newMail, getReplyTo(originalMail), originalMail);
            setReversePath(newMail, getReversePath(originalMail), originalMail);
            setSender(newMail, getSender(originalMail), originalMail);
            setIsReply(newMail, isReply(originalMail), originalMail);
    
            newMail.getMessage().saveChanges();
            getMailetContext().sendMail(newMail);
        } finally {
            newMail.dispose();
        }

        // ghosting the original mail
        if(!getPassThrough(originalMail)) {
            originalMail.setState(Mail.GHOST);
        }