DSNBouncepublic 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>
|
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 |
Methods Summary |
---|
private java.lang.String | arrayToString(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.MimeBodyPart | createAttachedOriginal(org.apache.mailet.Mail originalMail, int attachmentType)Create a MimeBodyPart with the original Mail as Attachment
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.MimeBodyPart | createDSN(org.apache.mailet.Mail originalMail)creates the DSN-bodypart for automated processing
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.MimeBodyPart | createTextMsg(org.apache.mailet.Mail originalMail)Create a MimeBodyPart with a textual description for human readers.
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 int | getAttachmentType()
return getTypeCode(getInitParameter("attachment","message"));
| protected java.lang.String | getErrorMsg(javax.mail.MessagingException me)Utility method for getting the error message from the (nested) exception.
if (me.getNextException() == null) {
return me.getMessage().trim();
} else {
Exception ex1 = me.getNextException();
return ex1.getMessage().trim();
}
| public java.lang.String | getMailetInfo()
return "DSNBounce Mailet";
| protected java.util.Collection | getRecipients()
Collection newRecipients = new HashSet();
newRecipients.add(SpecialAddress.REVERSE_PATH);
return newRecipients;
| protected org.apache.mailet.MailAddress | getReversePath(org.apache.mailet.Mail originalMail)
return SpecialAddress.NULL;
| protected java.lang.String | getStatus(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
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()
InternetAddress[] apparentlyTo = new InternetAddress[1];
apparentlyTo[0] = SpecialAddress.REVERSE_PATH.toInternetAddress();
return apparentlyTo;
| public void | init()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.String | newName(org.apache.mailet.Mail mail)Create a unique new primary key 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 void | service(org.apache.mailet.Mail originalMail)Service does the hard work and bounces the originalMail in the format specified by RFC3464.
// 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);
}
|
|