1 /************************************************************************
2 * Copyright (c) 2000-2006 The Apache Software Foundation. *
3 * All rights reserved. *
4 * ------------------------------------------------------------------- *
5 * Licensed under the Apache License, Version 2.0 (the "License"); you *
6 * may not use this file except in compliance with the License. You *
7 * may obtain a copy of the License at: *
8 * *
9 * http://www.apache.org/licenses/LICENSE-2.0 *
10 * *
11 * Unless required by applicable law or agreed to in writing, software *
12 * distributed under the License is distributed on an "AS IS" BASIS, *
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or *
14 * implied. See the License for the specific language governing *
15 * permissions and limitations under the License. *
16 ***********************************************************************/
17
18 package org.apache.james.transport.mailets;
19
20 import org.apache.james.Constants;
21 import org.apache.james.core.MailImpl;
22 import org.apache.james.util.mail.MimeMultipartReport;
23 import org.apache.james.util.mail.dsn.DSNStatus;
24 import org.apache.mailet.Mail;
25 import org.apache.mailet.MailAddress;
26 import org.apache.mailet.RFC2822Headers;
27 import org.apache.mailet.dates.RFC822DateFormat;
28 import org.apache.oro.text.regex.MalformedPatternException;
29 import org.apache.oro.text.regex.MatchResult;
30 import org.apache.oro.text.regex.Pattern;
31 import org.apache.oro.text.regex.Perl5Compiler;
32 import org.apache.oro.text.regex.Perl5Matcher;
33
34 import javax.mail.MessagingException;
35 import javax.mail.SendFailedException;
36 import javax.mail.Session;
37 import javax.mail.internet.InternetAddress;
38 import javax.mail.internet.MimeBodyPart;
39 import javax.mail.internet.MimeMessage;
40
41 import java.io.PrintWriter;
42 import java.io.StringWriter;
43 import java.net.ConnectException;
44 import java.net.InetAddress;
45 import java.net.SocketException;
46 import java.net.UnknownHostException;
47 import java.util.Collection;
48 import java.util.Date;
49 import java.util.HashSet;
50 import java.util.Iterator;
51
52
53
54
55 /***
56 *
57 * <P>Generates a Delivery Status Notification (DSN)
58 * Note that this is different than a mail-client's
59 * reply, which would use the Reply-To or From header.</P>
60 * <P>Bounced messages are attached in their entirety (headers and
61 * content) and the resulting MIME part type is "message/rfc822".<BR>
62 * The reverse-path and the Return-Path header of the response is set to "null" ("<>"),
63 * meaning that no reply should be sent.</P>
64 * <P>A sender of the notification message can optionally be specified.
65 * If one is not specified, the postmaster's address will be used.<BR>
66 * <P>Supports the <CODE>passThrough</CODE> init parameter (true if missing).</P>
67 *
68 * <P>Sample configuration:</P>
69 * <PRE><CODE>
70 * <mailet match="All" class="DSNBounce">
71 * <sender><I>an address or postmaster or sender or unaltered,
72 default=postmaster</I></sender>
73 * <prefix><I>optional subject prefix prepended to the original
74 message</I></prefix>
75 * <attachment><I>message, heads or none, default=message</I></attachment>
76 * <messageString><I>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] ... </I></messageString>
77 * <passThrough><I>true or false, default=true</I></passThrough>
78 * <debug><I>true or false, default=false</I></debug>
79 * </mailet>
80 * </CODE></PRE>
81 *
82 * @see org.apache.james.transport.mailets.AbstractNotify
83 */
84
85
86
87 public class DSNBounce extends AbstractNotify {
88
89
90 private static final RFC822DateFormat rfc822DateFormat = new RFC822DateFormat();
91
92
93 private static final java.util.Random random = new java.util.Random();
94
95
96 private static Pattern statusPattern;
97
98 private static Pattern diagPattern;
99
100 private static final String MACHINE_PATTERN = "[machine]";
101
102 private String messageString = null;
103
104
105
106
107
108 static {
109 Perl5Compiler compiler = new Perl5Compiler();
110 String status_pattern_string = ".*//s*([245]//.//d{1,3}//.//d{1,3}).*//s*";
111 String diag_pattern_string = "^//d{3}//s.*$";
112 try {
113 statusPattern = compiler.
114 compile(status_pattern_string, Perl5Compiler.READ_ONLY_MASK);
115 } catch(MalformedPatternException mpe) {
116
117 System.err.println ("Malformed pattern: " + status_pattern_string);
118 mpe.printStackTrace (System.err);
119 }
120 try {
121 diagPattern = compiler.
122 compile(diag_pattern_string, Perl5Compiler.READ_ONLY_MASK);
123 } catch(MalformedPatternException mpe) {
124
125 System.err.println ("Malformed pattern: " + diag_pattern_string);
126 }
127 }
128
129 /***
130 * Initialize the mailet
131 */
132 public void init() throws MessagingException {
133 super.init();
134 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");
135 }
136
137 /***
138 * Service does the hard work and bounces the originalMail in the format specified by RFC3464.
139 *
140 * @param originalMail the mail to bounce
141 * @throws MessagingException if a problem arises formulating the redirected mail
142 *
143 * @see org.apache.mailet.Mailet#service(org.apache.mailet.Mail)
144 */
145 public void service(Mail originalMail) throws MessagingException {
146
147
148
149 MailImpl newMail = new MailImpl(originalMail,newName(originalMail));
150 try {
151
152
153
154 try {
155 newMail.setRemoteAddr(java.net.InetAddress.getLocalHost().getHostAddress());
156 newMail.setRemoteHost(java.net.InetAddress.getLocalHost().getHostName());
157 } catch (java.net.UnknownHostException _) {
158 newMail.setRemoteAddr("127.0.0.1");
159 newMail.setRemoteHost("localhost");
160 }
161
162 if (originalMail.getSender() == null) {
163 if (isDebug)
164 log("Processing a bounce request for a message with an empty reverse-path. No bounce will be sent.");
165 if(!getPassThrough(originalMail)) {
166 originalMail.setState(Mail.GHOST);
167 }
168 return;
169 }
170
171 MailAddress reversePath = originalMail.getSender();
172 if (isDebug)
173 log("Processing a bounce request for a message with a reverse path. The bounce will be sent to " + reversePath);
174
175 Collection newRecipients = new HashSet();
176 newRecipients.add(reversePath);
177 newMail.setRecipients(newRecipients);
178
179 if (isDebug) {
180 log("New mail - sender: " + newMail.getSender()
181 + ", recipients: " +
182 arrayToString(newMail.getRecipients().toArray())
183 + ", name: " + newMail.getName()
184 + ", remoteHost: " + newMail.getRemoteHost()
185 + ", remoteAddr: " + newMail.getRemoteAddr()
186 + ", state: " + newMail.getState()
187 + ", lastUpdated: " + newMail.getLastUpdated()
188 + ", errorMessage: " + newMail.getErrorMessage());
189 }
190
191
192 MimeMessage newMessage =
193 new MimeMessage(Session.getDefaultInstance(System.getProperties(),
194 null));
195
196 MimeMultipartReport multipart = new MimeMultipartReport ();
197 multipart.setReportType ("delivery-status");
198
199
200 MimeBodyPart part1 = createTextMsg(originalMail);
201 multipart.addBodyPart(part1);
202
203
204 MimeBodyPart part2 = createDSN(originalMail);
205 multipart.addBodyPart(part2);
206
207
208
209 if (getAttachmentType() != NONE) {
210 MimeBodyPart part3 = createAttachedOriginal(originalMail,getAttachmentType());
211 multipart.addBodyPart(part3);
212 }
213
214
215
216 newMessage.setContent(multipart);
217 newMessage.setHeader(RFC2822Headers.CONTENT_TYPE, multipart.getContentType());
218 newMail.setMessage(newMessage);
219
220
221 setRecipients(newMail, getRecipients(originalMail), originalMail);
222 setTo(newMail, getTo(originalMail), originalMail);
223 setSubjectPrefix(newMail, getSubjectPrefix(originalMail), originalMail);
224 if(newMail.getMessage().getHeader(RFC2822Headers.DATE) == null) {
225 newMail.getMessage().setHeader(RFC2822Headers.DATE,rfc822DateFormat.format(new Date()));
226 }
227 setReplyTo(newMail, getReplyTo(originalMail), originalMail);
228 setReversePath(newMail, getReversePath(originalMail), originalMail);
229 setSender(newMail, getSender(originalMail), originalMail);
230 setIsReply(newMail, isReply(originalMail), originalMail);
231
232 newMail.getMessage().saveChanges();
233 getMailetContext().sendMail(newMail);
234 } finally {
235 newMail.dispose();
236 }
237
238
239 if(!getPassThrough(originalMail)) {
240 originalMail.setState(Mail.GHOST);
241 }
242 }
243
244 /***
245 * Create a MimeBodyPart with a textual description for human readers.
246 *
247 * @param originalMail
248 * @return MimeBodyPart
249 * @throws MessagingException
250 */
251 protected MimeBodyPart createTextMsg(Mail originalMail)
252 throws MessagingException {
253 MimeBodyPart part1 = new MimeBodyPart();
254 StringWriter sout = new StringWriter();
255 PrintWriter out = new PrintWriter(sout, true);
256 String machine = "[unknown]";
257 try {
258 InetAddress me = InetAddress.getLocalHost();
259 machine = me.getHostName();
260 } catch(Exception e){
261 machine = "[address unknown]";
262 }
263
264 StringBuffer bounceBuffer =
265 new StringBuffer(128).append (messageString);
266 int m_idx_begin = messageString.indexOf(MACHINE_PATTERN);
267 if (m_idx_begin != -1) {
268 bounceBuffer.replace (m_idx_begin,
269 m_idx_begin+MACHINE_PATTERN.length(),
270 machine);
271 }
272 out.println(bounceBuffer.toString());
273 out.println("Failed recipient(s):");
274 for (Iterator i = originalMail.getRecipients().iterator(); i.hasNext(); ) {
275 out.println(i.next());
276 }
277 MessagingException ex = (MessagingException)originalMail.getAttribute("delivery-error");
278 out.println();
279 out.println("Error message:");
280 out.println(getErrorMsg(ex));
281 out.println();
282
283 part1.setText(sout.toString());
284 return part1;
285 }
286
287 /***
288 * creates the DSN-bodypart for automated processing
289 *
290 * @param originalMail
291 * @return MimeBodyPart dsn-bodypart
292 * @throws MessagingException
293 */
294 protected MimeBodyPart createDSN(Mail originalMail) throws MessagingException {
295 MimeBodyPart dsn = new MimeBodyPart();
296 StringWriter sout = new StringWriter();
297 PrintWriter out = new PrintWriter(sout, true);
298 String nameType = null;
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316 nameType = "dns";
317 try {
318 String myAddress =
319 (String)getMailetContext().getAttribute(Constants.HELLO_NAME);
320
321
322
323 out.println("Reporting-MTA: "+nameType+"; "+myAddress);
324 } catch(Exception e){
325
326 log("WARNING: sending DSN without required Reporting-MTA Address");
327 }
328
329
330
331
332 out.println("Received-From-MTA: "+nameType+"; "+originalMail.getRemoteHost());
333
334
335
336
337
338
339
340 Iterator recipients = originalMail.getRecipients().iterator();
341 while (recipients.hasNext())
342 {
343 MailAddress rec = (MailAddress)recipients.next();
344 String addressType = "rfc822";
345
346
347 out.println();
348
349
350
351
352
353 out.println("Final-Recipient: "+addressType+"; "+rec.toString());
354
355
356
357
358 out.println("Action: failed");
359
360
361
362
363 MessagingException ex =
364 (MessagingException) originalMail.getAttribute("delivery-error");
365 out.println("Status: "+getStatus(ex));
366
367
368
369
370
371 String diagnosticType = null;
372
373
374
375
376 String diagnosticCode = getErrorMsg(ex);
377
378
379 Perl5Matcher diagMatcher = new Perl5Matcher();
380 boolean smtpDiagCodeAvailable =
381 diagMatcher.matches(diagnosticCode, diagPattern);
382 if (smtpDiagCodeAvailable){
383 diagnosticType = "smtp";
384 } else {
385 diagnosticType = "X-James";
386 }
387 out.println("Diagnostic-Code: "+diagnosticType+"; "+diagnosticCode);
388
389
390 out.println("Last-Attempt-Date: "+
391 rfc822DateFormat.format(originalMail.getLastUpdated()));
392
393
394
395
396
397
398 }
399
400
401
402
403
404
405
406
407 dsn.setContent(sout.toString(), "text/plain");
408 dsn.setHeader("Content-Type","message/delivery-status");
409 dsn.setDescription("Delivery Status Notification");
410 dsn.setFileName("status.dat");
411 return dsn;
412 }
413
414 /***
415 * Create a MimeBodyPart with the original Mail as Attachment
416 *
417 * @param originalMail
418 * @return MimeBodyPart
419 * @throws MessagingException
420 */
421 protected MimeBodyPart createAttachedOriginal(Mail originalMail, int attachmentType)
422 throws MessagingException {
423 MimeBodyPart part = new MimeBodyPart();
424 MimeMessage originalMessage = originalMail.getMessage();
425
426 if (attachmentType == HEADS) {
427 part.setContent(getMessageHeaders(originalMessage), "text/plain");
428 part.setHeader("Content-Type","text/rfc822-headers");
429 } else {
430 part.setContent(originalMessage, "message/rfc822");
431 }
432
433 if ((originalMessage.getSubject() != null) &&
434 (originalMessage.getSubject().trim().length() > 0)) {
435 part.setFileName(originalMessage.getSubject().trim());
436 } else {
437 part.setFileName("No Subject");
438 }
439 part.setDisposition("Attachment");
440 return part;
441 }
442
443 /***
444 * Guessing status code by the exception provided.
445 * This method should use the status attribute when the
446 * SMTP-handler somewhen provides it
447 *
448 * @param MessagingException
449 * @return status code
450 */
451 protected String getStatus(MessagingException me) {
452 if (me.getNextException() == null) {
453 String mess = me.getMessage();
454 Perl5Matcher m = new Perl5Matcher();
455 StringBuffer sb = new StringBuffer();
456 if (m.matches(mess, statusPattern)) {
457 MatchResult res = m.getMatch();
458 sb.append(res.group(1));
459 return sb.toString();
460 }
461
462 if (mess.startsWith("There are no DNS entries for the hostname"))
463 return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.ADDRESS_SYSTEM);
464
465
466
467 if (mess.equals("No mail server(s) available at this time."))
468 return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_NO_ANSWER);
469
470
471 return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.UNDEFINED_STATUS);
472 } else {
473 Exception ex1 = me.getNextException();
474 Perl5Matcher m = new Perl5Matcher ();
475 StringBuffer sb = new StringBuffer();
476 if (m.matches(ex1.getMessage(), statusPattern)) {
477 MatchResult res = m.getMatch();
478 sb.append(res.group(1));
479 return sb.toString();
480 } else if (ex1 instanceof SendFailedException) {
481
482 int smtpCode = 0;
483 try {
484 smtpCode = Integer.parseInt(ex1.getMessage().substring(0,3));
485 } catch(NumberFormatException e) {
486 }
487
488 switch (smtpCode) {
489
490
491 case 450: return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.MAILBOX_OTHER);
492
493 case 451: return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.SYSTEM_OTHER);
494
495 case 452: return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.SYSTEM_FULL);
496
497 case 500: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_SYNTAX);
498
499 case 501: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_ARG);
500
501 case 502: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_CMD);
502
503 case 503: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_CMD);
504
505 case 504: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_ARG);
506
507 case 550: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.MAILBOX_OTHER);
508
509
510 case 551: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.SECURITY_AUTH);
511
512 case 552: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.MAILBOX_FULL);
513
514 case 553: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.ADDRESS_SYNTAX);
515
516 case 554: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.UNDEFINED_STATUS);
517
518 case 571: return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.SECURITY_AUTH);
519
520 default:
521
522
523 if (ex1.getMessage().startsWith("4")) {
524 return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.DELIVERY_OTHER);
525 } else return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_OTHER);
526 }
527
528 } else if (ex1 instanceof UnknownHostException) {
529
530 return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.ADDRESS_SYSTEM);
531 } else if (ex1 instanceof ConnectException) {
532
533 return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_CONNECTION);
534 } else if (ex1 instanceof SocketException) {
535
536 return DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.NETWORK_CONNECTION);
537 } else {
538
539 return DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.UNDEFINED_STATUS);
540 }
541 }
542 }
543
544 /***
545 * Utility method for getting the error message from the (nested) exception.
546 * @param MessagingException
547 * @return error message
548 */
549 protected String getErrorMsg(MessagingException me) {
550 if (me.getNextException() == null) {
551 return me.getMessage().trim();
552 } else {
553 Exception ex1 = me.getNextException();
554 return ex1.getMessage().trim();
555 }
556 }
557
558 /***
559 * Utility method for obtaining a string representation of an array of Objects.
560 */
561 private String arrayToString(Object[] array) {
562 if (array == null) {
563 return "null";
564 }
565 StringBuffer sb = new StringBuffer(1024);
566 sb.append("[");
567 for (int i = 0; i < array.length; i++) {
568 if (i > 0) {
569 sb.append(",");
570 }
571 sb.append(array[i]);
572 }
573 sb.append("]");
574 return sb.toString();
575 }
576
577 /***
578 * Create a unique new primary key name.
579 *
580 * @param mail the mail to use as the basis for the new mail name
581 * @return a new name
582 */
583 protected String newName(Mail mail) throws MessagingException {
584 String oldName = mail.getName();
585
586
587
588
589
590 if (oldName.length() > 76) {
591 int count = 0;
592 int index = 0;
593 while ((index = oldName.indexOf('!', index + 1)) >= 0) {
594 count++;
595 }
596
597 if (count > 7) {
598 throw new MessagingException("Unable to create a new message name: too long."
599 + " Possible loop in config.xml.");
600 }
601 else {
602 oldName = oldName.substring(0, 76);
603 }
604 }
605
606 StringBuffer nameBuffer =
607 new StringBuffer(64)
608 .append(oldName)
609 .append("-!")
610 .append(random.nextInt(1048576));
611 return nameBuffer.toString();
612 }
613
614
615
616 public String getMailetInfo() {
617 return "DSNBounce Mailet";
618 }
619
620
621
622
623 /*** Gets the expected init parameters. */
624 protected String[] getAllowedInitParameters() {
625 String[] allowedArray = {
626 "debug",
627 "passThrough",
628 "messageString",
629 "attachment",
630 "sender",
631 "prefix"
632 };
633 return allowedArray;
634 }
635
636 /***
637 * @return the <CODE>attachment</CODE> init parameter, or <CODE>MESSAGE</CODE> if missing
638 */
639 protected int getAttachmentType() throws MessagingException {
640 return getTypeCode(getInitParameter("attachment","message"));
641 }
642
643
644 /***
645 * @return <CODE>SpecialAddress.REVERSE_PATH</CODE>
646 */
647 protected Collection getRecipients() {
648 Collection newRecipients = new HashSet();
649 newRecipients.add(SpecialAddress.REVERSE_PATH);
650 return newRecipients;
651 }
652
653 /***
654 * @return <CODE>SpecialAddress.REVERSE_PATH</CODE>
655 */
656 protected InternetAddress[] getTo() {
657 InternetAddress[] apparentlyTo = new InternetAddress[1];
658 apparentlyTo[0] = SpecialAddress.REVERSE_PATH.toInternetAddress();
659 return apparentlyTo;
660 }
661
662 /***
663 * @return <CODE>SpecialAddress.NULL</CODE> (the meaning of bounce)
664 */
665 protected MailAddress getReversePath(Mail originalMail) {
666 return SpecialAddress.NULL;
667 }
668
669
670
671
672
673 }