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