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.avalon.cornerstone.services.store.Store;
21 import org.apache.avalon.framework.configuration.DefaultConfiguration;
22 import org.apache.avalon.framework.container.ContainerUtil;
23 import org.apache.avalon.framework.service.ServiceException;
24 import org.apache.avalon.framework.service.ServiceManager;
25 import org.apache.james.Constants;
26 import org.apache.james.services.SpoolRepository;
27 import org.apache.mailet.GenericMailet;
28 import org.apache.mailet.HostAddress;
29 import org.apache.mailet.Mail;
30 import org.apache.mailet.MailAddress;
31 import org.apache.mailet.MailetContext;
32 import org.apache.oro.text.regex.MalformedPatternException;
33 import org.apache.oro.text.regex.MatchResult;
34 import org.apache.oro.text.regex.Pattern;
35 import org.apache.oro.text.regex.Perl5Compiler;
36 import org.apache.oro.text.regex.Perl5Matcher;
37
38 import com.sun.mail.smtp.SMTPAddressFailedException;
39 import com.sun.mail.smtp.SMTPAddressSucceededException;
40 import com.sun.mail.smtp.SMTPSendFailedException;
41 import com.sun.mail.smtp.SMTPTransport;
42
43 import javax.mail.Address;
44 import javax.mail.MessagingException;
45 import javax.mail.SendFailedException;
46 import javax.mail.Session;
47 import javax.mail.Transport;
48 import javax.mail.internet.InternetAddress;
49 import javax.mail.internet.MimeMessage;
50 import javax.mail.internet.MimeMultipart;
51 import javax.mail.internet.MimePart;
52 import javax.mail.internet.ParseException;
53
54 import java.io.IOException;
55 import java.io.PrintWriter;
56 import java.io.StringWriter;
57 import java.net.ConnectException;
58 import java.net.InetAddress;
59 import java.net.SocketException;
60 import java.net.UnknownHostException;
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.Collection;
64 import java.util.Date;
65 import java.util.HashMap;
66 import java.util.Hashtable;
67 import java.util.Iterator;
68 import java.util.Locale;
69 import java.util.Properties;
70 import java.util.StringTokenizer;
71 import java.util.Vector;
72
73
74 /***
75 * Receives a MessageContainer from JamesSpoolManager and takes care of delivery
76 * the message to remote hosts. If for some reason mail can't be delivered
77 * store it in the "outgoing" Repository and set an Alarm. After the next "delayTime" the
78 * Alarm will wake the servlet that will try to send it again. After "maxRetries"
79 * the mail will be considered undeliverable and will be returned to sender.
80 *
81 * TO DO (in priority):
82 * 1. Support a gateway (a single server where all mail will be delivered) (DONE)
83 * 2. Provide better failure messages (DONE)
84 * 3. More efficiently handle numerous recipients
85 * 4. Migrate to use Phoenix for the delivery threads
86 *
87 * You really want to read the JavaMail documentation if you are
88 * working in here, and you will want to view the list of JavaMail
89 * attributes, which are documented here:
90 *
91 * http://java.sun.com/products/javamail/1.3/docs/javadocs/com/sun/mail/smtp/package-summary.html
92 *
93 * as well as other places.
94 *
95 * @version CVS $Revision: 450877 $ $Date: 2006-09-28 14:49:38 +0000 (gio, 28 set 2006) $
96 */
97 public class RemoteDelivery extends GenericMailet implements Runnable {
98
99 private static final long DEFAULT_DELAY_TIME = 21600000;
100 private static final String PATTERN_STRING =
101 "//s*([0-9]*//s*[//*])?//s*([0-9]+)//s*([a-z,A-Z]*)//s*";
102
103
104 private static Pattern PATTERN = null;
105 private static final HashMap MULTIPLIERS = new HashMap (10);
106
107
108
109
110
111
112
113 static {
114 try {
115 Perl5Compiler compiler = new Perl5Compiler();
116 PATTERN = compiler.compile(PATTERN_STRING, Perl5Compiler.READ_ONLY_MASK);
117 } catch(MalformedPatternException mpe) {
118
119 System.err.println ("Malformed pattern: " + PATTERN_STRING);
120 mpe.printStackTrace (System.err);
121 }
122
123 MULTIPLIERS.put ("msec", new Integer (1));
124 MULTIPLIERS.put ("msecs", new Integer (1));
125 MULTIPLIERS.put ("sec", new Integer (1000));
126 MULTIPLIERS.put ("secs", new Integer (1000));
127 MULTIPLIERS.put ("minute", new Integer (1000*60));
128 MULTIPLIERS.put ("minutes", new Integer (1000*60));
129 MULTIPLIERS.put ("hour", new Integer (1000*60*60));
130 MULTIPLIERS.put ("hours", new Integer (1000*60*60));
131 MULTIPLIERS.put ("day", new Integer (1000*60*60*24));
132 MULTIPLIERS.put ("days", new Integer (1000*60*60*24));
133 }
134
135 /***
136 * This filter is used in the accept call to the spool.
137 * It will select the next mail ready for processing according to the mails
138 * retrycount and lastUpdated time
139 **/
140 private class MultipleDelayFilter implements SpoolRepository.AcceptFilter
141 {
142 /***
143 * holds the time to wait for the youngest mail to get ready for processing
144 **/
145 long youngest = 0;
146
147 /***
148 * Uses the getNextDelay to determine if a mail is ready for processing based on the delivered parameters
149 * errorMessage (which holds the retrycount), lastUpdated and state
150 * @param key the name/key of the message
151 * @param state the mails state
152 * @param lastUpdated the mail was last written to the spool at this time.
153 * @param errorMessage actually holds the retrycount as a string (see failMessage below)
154 **/
155 public boolean accept (String key, String state, long lastUpdated, String errorMessage) {
156 if (state.equals(Mail.ERROR)) {
157
158 int retries = Integer.parseInt(errorMessage);
159
160
161 if (retries == 0) return true;
162
163 long delay = getNextDelay (retries);
164 long timeToProcess = delay + lastUpdated;
165
166
167 if (System.currentTimeMillis() > timeToProcess) {
168
169 return true;
170 } else {
171
172 if (youngest == 0 || youngest > timeToProcess) {
173
174 youngest = timeToProcess;
175 }
176 return false;
177 }
178 } else {
179
180 return true;
181 }
182 }
183
184 /***
185 * @return the optimal time the SpoolRepository.accept(AcceptFilter) method should wait before
186 * trying to find a mail ready for processing again.
187 **/
188 public long getWaitTime () {
189 if (youngest == 0) {
190 return 0;
191 } else {
192 long duration = youngest - System.currentTimeMillis();
193 youngest = 0;
194 return duration <= 0 ? 1 : duration;
195 }
196 }
197 }
198
199 /***
200 * Controls certain log messages
201 */
202 private boolean isDebug = false;
203
204 private SpoolRepository outgoing;
205 private long[] delayTimes;
206 private int maxRetries = 5;
207 private long smtpTimeout = 180000;
208 private boolean sendPartial = false;
209 private int connectionTimeout = 60000;
210 private int deliveryThreadCount = 1;
211 private Collection gatewayServer = null;
212 private String authUser = null;
213 private String authPass = null;
214 private String bindAddress = null;
215 private boolean isBindUsed = false;
216
217
218 private Collection deliveryThreads = new Vector();
219 private volatile boolean destroyed = false;
220 private String bounceProcessor = null;
221
222 private Perl5Matcher delayTimeMatcher;
223 private MultipleDelayFilter delayFilter = new MultipleDelayFilter ();
224
225
226 /***
227 * Initialize the mailet
228 */
229 public void init() throws MessagingException {
230 isDebug = (getInitParameter("debug") == null) ? false : new Boolean(getInitParameter("debug")).booleanValue();
231 ArrayList delay_times_list = new ArrayList();
232 try {
233 if (getInitParameter("delayTime") != null) {
234 delayTimeMatcher = new Perl5Matcher();
235 String delay_times = getInitParameter("delayTime");
236
237 StringTokenizer st = new StringTokenizer (delay_times,",");
238 while (st.hasMoreTokens()) {
239 String delay_time = st.nextToken();
240 delay_times_list.add (new Delay(delay_time));
241 }
242 } else {
243
244 delay_times_list.add (new Delay());
245 }
246 } catch (Exception e) {
247 log("Invalid delayTime setting: " + getInitParameter("delayTime"));
248 }
249 try {
250 if (getInitParameter("maxRetries") != null) {
251 maxRetries = Integer.parseInt(getInitParameter("maxRetries"));
252 }
253
254 int total_attempts = calcTotalAttempts (delay_times_list);
255 if (total_attempts > maxRetries) {
256 log("Total number of delayTime attempts exceeds maxRetries specified. Increasing maxRetries from "+maxRetries+" to "+total_attempts);
257 maxRetries = total_attempts;
258 } else {
259 int extra = maxRetries - total_attempts;
260 if (extra != 0) {
261 log("maxRetries is larger than total number of attempts specified. Increasing last delayTime with "+extra+" attempts ");
262
263 if (delay_times_list.size() != 0) {
264 Delay delay = (Delay)delay_times_list.get (delay_times_list.size()-1);
265 delay.setAttempts (delay.getAttempts()+extra);
266 log("Delay of "+delay.getDelayTime()+" msecs is now attempted: "+delay.getAttempts()+" times");
267 } else {
268 log ("NO, delaytimes cannot continue");
269 }
270 }
271 }
272 delayTimes = expandDelays (delay_times_list);
273
274 } catch (Exception e) {
275 log("Invalid maxRetries setting: " + getInitParameter("maxRetries"));
276 }
277 try {
278 if (getInitParameter("timeout") != null) {
279 smtpTimeout = Integer.parseInt(getInitParameter("timeout"));
280 }
281 } catch (Exception e) {
282 log("Invalid timeout setting: " + getInitParameter("timeout"));
283 }
284
285 try {
286 if (getInitParameter("connectiontimeout") != null) {
287 connectionTimeout = Integer.parseInt(getInitParameter("connectiontimeout"));
288 }
289 } catch (Exception e) {
290 log("Invalid timeout setting: " + getInitParameter("timeout"));
291 }
292 sendPartial = (getInitParameter("sendpartial") == null) ? false : new Boolean(getInitParameter("sendpartial")).booleanValue();
293
294 bounceProcessor = getInitParameter("bounceProcessor");
295
296 String gateway = getInitParameter("gateway");
297 String gatewayPort = getInitParameter("gatewayPort");
298
299 if (gateway != null) {
300 gatewayServer = new ArrayList();
301 StringTokenizer st = new StringTokenizer(gateway, ",") ;
302 while (st.hasMoreTokens()) {
303 String server = st.nextToken().trim() ;
304 if (server.indexOf(':') < 0 && gatewayPort != null) {
305 server += ":";
306 server += gatewayPort;
307 }
308
309 if (isDebug) log("Adding SMTP gateway: " + server) ;
310 gatewayServer.add(server);
311 }
312 authUser = getInitParameter("gatewayusername");
313 authPass = getInitParameter("gatewayPassword");
314 }
315
316 ServiceManager compMgr = (ServiceManager)getMailetContext().getAttribute(Constants.AVALON_COMPONENT_MANAGER);
317 String outgoingPath = getInitParameter("outgoing");
318 if (outgoingPath == null) {
319 outgoingPath = "file:///../var/mail/outgoing";
320 }
321
322 try {
323
324 Store mailstore = (Store) compMgr.lookup(Store.ROLE);
325
326 DefaultConfiguration spoolConf
327 = new DefaultConfiguration("repository", "generated:RemoteDelivery.java");
328 spoolConf.setAttribute("destinationURL", outgoingPath);
329 spoolConf.setAttribute("type", "SPOOL");
330 outgoing = (SpoolRepository) mailstore.select(spoolConf);
331 } catch (ServiceException cnfe) {
332 log("Failed to retrieve Store component:" + cnfe.getMessage());
333 } catch (Exception e) {
334 log("Failed to retrieve Store component:" + e.getMessage());
335 }
336
337
338 try {
339 deliveryThreadCount = Integer.parseInt(getInitParameter("deliveryThreads"));
340 } catch (Exception e) {
341 }
342 for (int i = 0; i < deliveryThreadCount; i++) {
343 StringBuffer nameBuffer =
344 new StringBuffer(32)
345 .append("Remote delivery thread (")
346 .append(i)
347 .append(")");
348 Thread t = new Thread(this, nameBuffer.toString());
349 t.start();
350 deliveryThreads.add(t);
351 }
352
353 bindAddress = getInitParameter("bind");
354 isBindUsed = bindAddress != null;
355 try {
356 if (isBindUsed) RemoteDeliverySocketFactory.setBindAdress(bindAddress);
357 } catch (UnknownHostException e) {
358 log("Invalid bind setting (" + bindAddress + "): " + e.toString());
359 }
360 }
361
362
363
364
365 private void logSendFailedException(SendFailedException sfe) {
366 if (isDebug) {
367 MessagingException me = sfe;
368 if (me instanceof SMTPSendFailedException) {
369 SMTPSendFailedException ssfe = (SMTPSendFailedException)me;
370 log("SMTP SEND FAILED:");
371 log(ssfe.toString());
372 log(" Command: " + ssfe.getCommand());
373 log(" RetCode: " + ssfe.getReturnCode());
374 log(" Response: " + ssfe.getMessage());
375 } else {
376 log("Send failed: " + me.toString());
377 }
378 Exception ne;
379 while ((ne = me.getNextException()) != null && ne instanceof MessagingException) {
380 me = (MessagingException)ne;
381 if (me instanceof SMTPAddressFailedException) {
382 SMTPAddressFailedException e = (SMTPAddressFailedException)me;
383 log("ADDRESS FAILED:");
384 log(e.toString());
385 log(" Address: " + e.getAddress());
386 log(" Command: " + e.getCommand());
387 log(" RetCode: " + e.getReturnCode());
388 log(" Response: " + e.getMessage());
389 } else if (me instanceof SMTPAddressSucceededException) {
390 log("ADDRESS SUCCEEDED:");
391 SMTPAddressSucceededException e = (SMTPAddressSucceededException)me;
392 log(e.toString());
393 log(" Address: " + e.getAddress());
394 log(" Command: " + e.getCommand());
395 log(" RetCode: " + e.getReturnCode());
396 log(" Response: " + e.getMessage());
397 }
398 }
399 }
400 }
401
402 /***
403 * We can assume that the recipients of this message are all going to the same
404 * mail server. We will now rely on the DNS server to do DNS MX record lookup
405 * and try to deliver to the multiple mail servers. If it fails, it should
406 * throw an exception.
407 *
408 * Creation date: (2/24/00 11:25:00 PM)
409 * @param mail org.apache.james.core.MailImpl
410 * @param session javax.mail.Session
411 * @return boolean Whether the delivery was successful and the message can be deleted
412 */
413 private boolean deliver(Mail mail, Session session) {
414 try {
415 if (isDebug) {
416 log("Attempting to deliver " + mail.getName());
417 }
418 MimeMessage message = mail.getMessage();
419
420
421 Collection recipients = mail.getRecipients();
422 InternetAddress addr[] = new InternetAddress[recipients.size()];
423 int j = 0;
424 for (Iterator i = recipients.iterator(); i.hasNext(); j++) {
425 MailAddress rcpt = (MailAddress)i.next();
426 addr[j] = rcpt.toInternetAddress();
427 }
428
429 if (addr.length <= 0) {
430 log("No recipients specified... not sure how this could have happened.");
431 return true;
432 }
433
434
435
436 Iterator targetServers = null;
437 if (gatewayServer == null) {
438 MailAddress rcpt = (MailAddress) recipients.iterator().next();
439 String host = rcpt.getHost();
440
441
442 targetServers = getMailetContext().getSMTPHostAddresses(host);
443 if (!targetServers.hasNext()) {
444 log("No mail server found for: " + host);
445 StringBuffer exceptionBuffer =
446 new StringBuffer(128)
447 .append("There are no DNS entries for the hostname ")
448 .append(host)
449 .append(". I cannot determine where to send this message.");
450 return failMessage(mail, new MessagingException(exceptionBuffer.toString()), false);
451 }
452 } else {
453 targetServers = getGatewaySMTPHostAddresses(gatewayServer);
454 }
455
456 MessagingException lastError = null;
457
458 while ( targetServers.hasNext()) {
459 try {
460 HostAddress outgoingMailServer = (HostAddress) targetServers.next();
461 StringBuffer logMessageBuffer =
462 new StringBuffer(256)
463 .append("Attempting delivery of ")
464 .append(mail.getName())
465 .append(" to host ")
466 .append(outgoingMailServer.getHostName())
467 .append(" at ")
468 .append(outgoingMailServer.getHost())
469 .append(" for addresses ")
470 .append(Arrays.asList(addr));
471 log(logMessageBuffer.toString());
472
473 Properties props = session.getProperties();
474 if (mail.getSender() == null) {
475 props.put("mail.smtp.from", "<>");
476 } else {
477 String sender = mail.getSender().toString();
478 props.put("mail.smtp.from", sender);
479 }
480
481
482
483
484
485
486
487 Transport transport = null;
488 try {
489 transport = session.getTransport(outgoingMailServer);
490 try {
491 if (authUser != null) {
492 transport.connect(outgoingMailServer.getHostName(), authUser, authPass);
493 } else {
494 transport.connect();
495 }
496 } catch (MessagingException me) {
497
498
499
500
501 log(me.getMessage());
502 continue;
503 }
504
505
506 if (transport instanceof SMTPTransport) {
507 SMTPTransport smtpTransport = (SMTPTransport) transport;
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536 try {
537 setEncodingIfMissing(message);
538 } catch (IOException e) {
539 log("Error while adding encoding information to the message", e);
540 }
541 } else {
542
543
544
545
546 try {
547 convertTo7Bit(message);
548 } catch (IOException e) {
549 log("Error during the conversion to 7 bit.", e);
550 }
551 }
552 transport.sendMessage(message, addr);
553 } finally {
554 if (transport != null) {
555 transport.close();
556 transport = null;
557 }
558 }
559 logMessageBuffer =
560 new StringBuffer(256)
561 .append("Mail (")
562 .append(mail.getName())
563 .append(") sent successfully to ")
564 .append(outgoingMailServer.getHostName())
565 .append(" at ")
566 .append(outgoingMailServer.getHost())
567 .append(" for ")
568 .append(mail.getRecipients());
569 log(logMessageBuffer.toString());
570 return true;
571 } catch (SendFailedException sfe) {
572 logSendFailedException(sfe);
573
574 if (sfe.getValidSentAddresses() != null) {
575 Address[] validSent = sfe.getValidSentAddresses();
576 if (validSent.length > 0) {
577 StringBuffer logMessageBuffer =
578 new StringBuffer(256)
579 .append("Mail (")
580 .append(mail.getName())
581 .append(") sent successfully for ")
582 .append(Arrays.asList(validSent));
583 log(logMessageBuffer.toString());
584 }
585 }
586
587
588 if (sfe instanceof SMTPSendFailedException) {
589 SMTPSendFailedException ssfe = (SMTPSendFailedException) sfe;
590
591 if (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599) throw sfe;
592 }
593
594 if (sfe.getValidUnsentAddresses() != null
595 && sfe.getValidUnsentAddresses().length > 0) {
596 if (isDebug) log("Send failed, " + sfe.getValidUnsentAddresses().length + " valid addresses remain, continuing with any other servers");
597 lastError = sfe;
598 continue;
599 } else {
600
601 throw sfe;
602 }
603 } catch (MessagingException me) {
604
605 StringBuffer exceptionBuffer =
606 new StringBuffer(256)
607 .append("Exception delivering message (")
608 .append(mail.getName())
609 .append(") - ")
610 .append(me.getMessage());
611 log(exceptionBuffer.toString());
612 if ((me.getNextException() != null) &&
613 (me.getNextException() instanceof java.io.IOException)) {
614
615
616
617
618 lastError = me;
619 continue;
620 }
621
622
623
624
625
626
627 throw me;
628 }
629 }
630
631
632
633
634
635
636 if (lastError != null) {
637 throw lastError;
638 }
639 } catch (SendFailedException sfe) {
640 logSendFailedException(sfe);
641
642 Collection recipients = mail.getRecipients();
643
644 boolean deleteMessage = false;
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667 if (sfe instanceof SMTPSendFailedException) {
668
669 SMTPSendFailedException ssfe = (SMTPSendFailedException) sfe;
670 deleteMessage = (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599);
671 } else {
672
673 MessagingException me = sfe;
674 Exception ne;
675 while ((ne = me.getNextException()) != null && ne instanceof MessagingException) {
676 me = (MessagingException)ne;
677 if (me instanceof SMTPAddressFailedException) {
678 SMTPAddressFailedException ssfe = (SMTPAddressFailedException)me;
679 deleteMessage = (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599);
680 }
681 }
682 }
683
684
685 if (isDebug) log("Recipients: " + recipients);
686
687 if (sfe.getInvalidAddresses() != null) {
688 Address[] address = sfe.getInvalidAddresses();
689 if (address.length > 0) {
690 recipients.clear();
691 for (int i = 0; i < address.length; i++) {
692 try {
693 recipients.add(new MailAddress(address[i].toString()));
694 } catch (ParseException pe) {
695
696
697
698 log("Can't parse invalid address: " + pe.getMessage());
699 }
700 }
701 if (isDebug) log("Invalid recipients: " + recipients);
702 deleteMessage = failMessage(mail, sfe, true);
703 }
704 }
705
706 if (sfe.getValidUnsentAddresses() != null) {
707 Address[] address = sfe.getValidUnsentAddresses();
708 if (address.length > 0) {
709 recipients.clear();
710 for (int i = 0; i < address.length; i++) {
711 try {
712 recipients.add(new MailAddress(address[i].toString()));
713 } catch (ParseException pe) {
714
715
716
717 log("Can't parse unsent address: " + pe.getMessage());
718 }
719 }
720 if (isDebug) log("Unsent recipients: " + recipients);
721 if (sfe instanceof SMTPSendFailedException) {
722 SMTPSendFailedException ssfe = (SMTPSendFailedException) sfe;
723 deleteMessage = failMessage(mail, sfe, ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599);
724 } else {
725 deleteMessage = failMessage(mail, sfe, false);
726 }
727 }
728 }
729
730 return deleteMessage;
731 } catch (MessagingException ex) {
732
733
734
735
736
737
738
739
740
741
742
743 return failMessage(mail, ex, ('5' == ex.getMessage().charAt(0)));
744 }
745
746
747
748
749
750
751
752 return failMessage(mail, new MessagingException("No mail server(s) available at this time."), false);
753 }
754
755 /***
756 * Converts a message to 7 bit.
757 *
758 * @param message
759 * @return
760 */
761 private void convertTo7Bit(MimePart part) throws MessagingException, IOException {
762 if (part.isMimeType("multipart/*")) {
763 MimeMultipart parts = (MimeMultipart) part.getContent();
764 int count = parts.getCount();
765 for (int i = 0; i < count; i++) {
766 convertTo7Bit((MimePart)parts.getBodyPart(i));
767 }
768 } else {
769 if (part.isMimeType("text/*")) {
770 part.setHeader("Content-Transfer-Encoding", "quoted-printable");
771 part.addHeader("X-MIME-Autoconverted", "from 8bit to quoted-printable by "+getMailetContext().getServerInfo());
772 } else {
773
774 part.setHeader("Content-Transfer-Encoding", "base64");
775 part.addHeader("X-MIME-Autoconverted", "from 8bit to base64 by "+getMailetContext().getServerInfo());
776 }
777 }
778 }
779
780 /***
781 * Adds an encoding information to each text mime part. This is a workaround
782 * for a javamail 1.3.2 bug: if a message is sent without encoding
783 * information a null pointer exception is thrown during the message
784 * delivery.
785 *
786 * @param part
787 * @throws MessagingException
788 * @throws IOException
789 */
790 private void setEncodingIfMissing(MimePart part) throws MessagingException, IOException {
791 if (part.isMimeType("text/*")) {
792 String enc = part.getEncoding();
793 if (enc == null) part.setHeader("Content-Transfer-Encoding", "7bit");
794 } else if (part.isMimeType("multipart/*")) {
795 Object content = part.getContent();
796 if (content instanceof MimeMultipart) {
797 MimeMultipart parts = (MimeMultipart) content;
798 int count = parts.getCount();
799 for (int i = 0; i < count; i++) {
800 setEncodingIfMissing((MimePart)parts.getBodyPart(i));
801 }
802 }
803 }
804 }
805
806 /***
807 * Insert the method's description here.
808 * Creation date: (2/25/00 1:14:18 AM)
809 * @param mail org.apache.james.core.MailImpl
810 * @param exception javax.mail.MessagingException
811 * @param boolean permanent
812 * @return boolean Whether the message failed fully and can be deleted
813 */
814 private boolean failMessage(Mail mail, MessagingException ex, boolean permanent) {
815 StringWriter sout = new StringWriter();
816 PrintWriter out = new PrintWriter(sout, true);
817 if (permanent) {
818 out.print("Permanent");
819 } else {
820 out.print("Temporary");
821 }
822 StringBuffer logBuffer =
823 new StringBuffer(64)
824 .append(" exception delivering mail (")
825 .append(mail.getName())
826 .append(": ");
827 out.print(logBuffer.toString());
828 if (isDebug) ex.printStackTrace(out);
829 log(sout.toString());
830 if (!permanent) {
831 if (!mail.getState().equals(Mail.ERROR)) {
832 mail.setState(Mail.ERROR);
833 mail.setErrorMessage("0");
834 mail.setLastUpdated(new Date());
835 }
836 int retries = Integer.parseInt(mail.getErrorMessage());
837 if (retries < maxRetries) {
838 logBuffer =
839 new StringBuffer(128)
840 .append("Storing message ")
841 .append(mail.getName())
842 .append(" into outgoing after ")
843 .append(retries)
844 .append(" retries");
845 log(logBuffer.toString());
846 ++retries;
847 mail.setErrorMessage(retries + "");
848 mail.setLastUpdated(new Date());
849 return false;
850 } else {
851 logBuffer =
852 new StringBuffer(128)
853 .append("Bouncing message ")
854 .append(mail.getName())
855 .append(" after ")
856 .append(retries)
857 .append(" retries");
858 log(logBuffer.toString());
859 }
860 }
861
862 if (mail.getSender() == null) {
863 log("Null Sender: no bounce will be generated for " + mail.getName());
864 return true;
865 }
866
867 if (bounceProcessor != null) {
868
869
870 mail.setAttribute("delivery-error", ex);
871 mail.setState(bounceProcessor);
872
873 MailetContext mc = getMailetContext();
874 try {
875 mc.sendMail(mail);
876 } catch (MessagingException e) {
877
878 log("Exception re-inserting failed mail: ", e);
879 }
880 } else {
881
882 bounce(mail, ex);
883 }
884 return true;
885 }
886
887 private void bounce(Mail mail, MessagingException ex) {
888 StringWriter sout = new StringWriter();
889 PrintWriter out = new PrintWriter(sout, true);
890 String machine = "[unknown]";
891 try {
892 InetAddress me = InetAddress.getLocalHost();
893 machine = me.getHostName();
894 } catch(Exception e){
895 machine = "[address unknown]";
896 }
897 StringBuffer bounceBuffer =
898 new StringBuffer(128)
899 .append("Hi. This is the James mail server at ")
900 .append(machine)
901 .append(".");
902 out.println(bounceBuffer.toString());
903 out.println("I'm afraid I wasn't able to deliver your message to the following addresses.");
904 out.println("This is a permanent error; I've given up. Sorry it didn't work out. Below");
905 out.println("I include the list of recipients and the reason why I was unable to deliver");
906 out.println("your message.");
907 out.println();
908 for (Iterator i = mail.getRecipients().iterator(); i.hasNext(); ) {
909 out.println(i.next());
910 }
911 if (ex.getNextException() == null) {
912 out.println(ex.getMessage().trim());
913 } else {
914 Exception ex1 = ex.getNextException();
915 if (ex1 instanceof SendFailedException) {
916 out.println("Remote mail server told me: " + ex1.getMessage().trim());
917 } else if (ex1 instanceof UnknownHostException) {
918 out.println("Unknown host: " + ex1.getMessage().trim());
919 out.println("This could be a DNS server error, a typo, or a problem with the recipient's mail server.");
920 } else if (ex1 instanceof ConnectException) {
921
922 out.println(ex1.getMessage().trim());
923 } else if (ex1 instanceof SocketException) {
924 out.println("Socket exception: " + ex1.getMessage().trim());
925 } else {
926 out.println(ex1.getMessage().trim());
927 }
928 }
929 out.println();
930
931 log("Sending failure message " + mail.getName());
932 try {
933 getMailetContext().bounce(mail, sout.toString());
934 } catch (MessagingException me) {
935 log("Encountered unexpected messaging exception while bouncing message: " + me.getMessage());
936 } catch (Exception e) {
937 log("Encountered unexpected exception while bouncing message: " + e.getMessage());
938 }
939 }
940
941 public String getMailetInfo() {
942 return "RemoteDelivery Mailet";
943 }
944
945 /***
946 * For this message, we take the list of recipients, organize these into distinct
947 * servers, and duplicate the message for each of these servers, and then call
948 * the deliver (messagecontainer) method for each server-specific
949 * messagecontainer ... that will handle storing it in the outgoing queue if needed.
950 *
951 * @param mail org.apache.mailet.Mail
952 */
953 public void service(Mail mail) throws MessagingException{
954
955 if (isDebug) {
956 log("Remotely delivering mail " + mail.getName());
957 }
958 Collection recipients = mail.getRecipients();
959
960 if (gatewayServer == null) {
961
962 Hashtable targets = new Hashtable();
963 for (Iterator i = recipients.iterator(); i.hasNext();) {
964 MailAddress target = (MailAddress)i.next();
965 String targetServer = target.getHost().toLowerCase(Locale.US);
966 Collection temp = (Collection)targets.get(targetServer);
967 if (temp == null) {
968 temp = new ArrayList();
969 targets.put(targetServer, temp);
970 }
971 temp.add(target);
972 }
973
974
975
976
977
978 String name = mail.getName();
979 for (Iterator i = targets.keySet().iterator(); i.hasNext(); ) {
980 String host = (String) i.next();
981 Collection rec = (Collection) targets.get(host);
982 if (isDebug) {
983 StringBuffer logMessageBuffer =
984 new StringBuffer(128)
985 .append("Sending mail to ")
986 .append(rec)
987 .append(" on host ")
988 .append(host);
989 log(logMessageBuffer.toString());
990 }
991 mail.setRecipients(rec);
992 StringBuffer nameBuffer =
993 new StringBuffer(128)
994 .append(name)
995 .append("-to-")
996 .append(host);
997 mail.setName(nameBuffer.toString());
998 outgoing.store(mail);
999
1000 }
1001 } else {
1002
1003 if (isDebug) {
1004 StringBuffer logMessageBuffer =
1005 new StringBuffer(128)
1006 .append("Sending mail to ")
1007 .append(mail.getRecipients())
1008 .append(" via ")
1009 .append(gatewayServer);
1010 log(logMessageBuffer.toString());
1011 }
1012
1013
1014 outgoing.store(mail);
1015 }
1016 mail.setState(Mail.GHOST);
1017 }
1018
1019
1020 public synchronized void destroy() {
1021
1022 destroyed = true;
1023
1024 for (Iterator i = deliveryThreads.iterator(); i.hasNext(); ) {
1025 Thread t = (Thread)i.next();
1026 t.interrupt();
1027 }
1028 notifyAll();
1029 }
1030
1031 /***
1032 * Handles checking the outgoing spool for new mail and delivering them if
1033 * there are any
1034 */
1035 public void run() {
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046 long stop = System.currentTimeMillis() + 60000;
1047 while ((getMailetContext().getAttribute(Constants.HELLO_NAME) == null)
1048 && stop > System.currentTimeMillis()) {
1049 try {
1050 Thread.sleep(1000);
1051 } catch (Exception ignored) {}
1052 }
1053
1054
1055 Properties props = new Properties();
1056
1057 props.put("mail.debug", "false");
1058
1059
1060 props.put("mail.smtp.ehlo", "true");
1061
1062
1063
1064
1065 props.setProperty("mail.smtp.allow8bitmime", "false");
1066
1067 props.put("mail.smtp.timeout", smtpTimeout + "");
1068
1069 props.put("mail.smtp.connectiontimeout", connectionTimeout + "");
1070 props.put("mail.smtp.sendpartial",String.valueOf(sendPartial));
1071
1072
1073 if (getMailetContext().getAttribute(Constants.HELLO_NAME) != null) {
1074 props.put("mail.smtp.localhost", getMailetContext().getAttribute(Constants.HELLO_NAME));
1075 }
1076 else {
1077 String defaultDomain = (String) getMailetContext().getAttribute(Constants.DEFAULT_DOMAIN);
1078 if (defaultDomain != null) {
1079 props.put("mail.smtp.localhost", defaultDomain);
1080 }
1081 }
1082
1083 if (isBindUsed) {
1084
1085
1086 props.put("mail.smtp.socketFactory.class",
1087 "org.apache.james.transport.mailets.RemoteDeliverySocketFactory");
1088
1089 props.put("mail.smtp.socketFactory.fallback", "false");
1090 }
1091
1092 if (authUser != null) {
1093 props.put("mail.smtp.auth","true");
1094 }
1095
1096 Session session = Session.getInstance(props, null);
1097 try {
1098 while (!Thread.interrupted() && !destroyed) {
1099 try {
1100 Mail mail = (Mail)outgoing.accept(delayFilter);
1101 String key = mail.getName();
1102 try {
1103 if (isDebug) {
1104 StringBuffer logMessageBuffer =
1105 new StringBuffer(128)
1106 .append(Thread.currentThread().getName())
1107 .append(" will process mail ")
1108 .append(key);
1109 log(logMessageBuffer.toString());
1110 }
1111 if (deliver(mail, session)) {
1112
1113 ContainerUtil.dispose(mail);
1114 outgoing.remove(key);
1115 } else {
1116
1117 outgoing.store(mail);
1118 ContainerUtil.dispose(mail);
1119
1120
1121 outgoing.unlock(key);
1122
1123
1124
1125
1126 }
1127
1128 mail = null;
1129 } catch (Exception e) {
1130
1131
1132
1133
1134
1135 ContainerUtil.dispose(mail);
1136 outgoing.remove(key);
1137 throw e;
1138 }
1139 } catch (Throwable e) {
1140 if (!destroyed) log("Exception caught in RemoteDelivery.run()", e);
1141 }
1142 }
1143 } finally {
1144
1145 Thread.interrupted();
1146 }
1147 }
1148
1149 /***
1150 * @param list holding Delay objects
1151 * @return the total attempts for all delays
1152 **/
1153 private int calcTotalAttempts (ArrayList list) {
1154 int sum = 0;
1155 Iterator i = list.iterator();
1156 while (i.hasNext()) {
1157 Delay delay = (Delay)i.next();
1158 sum += delay.getAttempts();
1159 }
1160 return sum;
1161 }
1162
1163 /***
1164 * This method expands an ArrayList containing Delay objects into an array holding the
1165 * only delaytime in the order.<p>
1166 * So if the list has 2 Delay objects the first having attempts=2 and delaytime 4000
1167 * the second having attempts=1 and delaytime=300000 will be expanded into this array:<p>
1168 * long[0] = 4000<p>
1169 * long[1] = 4000<p>
1170 * long[2] = 300000<p>
1171 * @param list the list to expand
1172 * @return the expanded list
1173 **/
1174 private long[] expandDelays (ArrayList list) {
1175 long[] delays = new long [calcTotalAttempts(list)];
1176 Iterator i = list.iterator();
1177 int idx = 0;
1178 while (i.hasNext()) {
1179 Delay delay = (Delay)i.next();
1180 for (int j=0; j<delay.getAttempts(); j++) {
1181 delays[idx++]= delay.getDelayTime();
1182 }
1183 }
1184 return delays;
1185 }
1186
1187 /***
1188 * This method returns, given a retry-count, the next delay time to use.
1189 * @param retry_count the current retry_count.
1190 * @return the next delay time to use, given the retry count
1191 **/
1192 private long getNextDelay (int retry_count) {
1193 if (retry_count > delayTimes.length) {
1194 return DEFAULT_DELAY_TIME;
1195 }
1196 return delayTimes[retry_count-1];
1197 }
1198
1199 /***
1200 * This class is used to hold a delay time and its corresponding number
1201 * of retries.
1202 **/
1203 private class Delay {
1204 private int attempts = 1;
1205 private long delayTime = DEFAULT_DELAY_TIME;
1206
1207
1208 /***
1209 * This constructor expects Strings of the form "[attempt\*]delaytime[unit]". <p>
1210 * The optional attempt is the number of tries this delay should be used (default = 1)
1211 * The unit if present must be one of (msec,sec,minute,hour,day) (default = msec)
1212 * The constructor multiplies the delaytime by the relevant multiplier for the unit,
1213 * so the delayTime instance variable is always in msec.
1214 * @param init_string the string to initialize this Delay object from
1215 **/
1216 public Delay (String init_string) throws MessagingException
1217 {
1218 String unit = "msec";
1219 if (delayTimeMatcher.matches (init_string, PATTERN)) {
1220 MatchResult res = delayTimeMatcher.getMatch ();
1221
1222
1223
1224
1225
1226 if (res.group(1) != null && !res.group(1).equals ("")) {
1227
1228 String attempt_match = res.group(1);
1229
1230 attempt_match = attempt_match.substring (0,attempt_match.length()-1).trim();
1231 attempts = Integer.parseInt (attempt_match);
1232 }
1233
1234 delayTime = Long.parseLong (res.group(2));
1235
1236 if (!res.group(3).equals ("")) {
1237
1238 unit = res.group(3).toLowerCase(Locale.US);
1239 }
1240 } else {
1241 throw new MessagingException(init_string+" does not match "+PATTERN_STRING);
1242 }
1243 if (MULTIPLIERS.get (unit)!=null) {
1244 int multiplier = ((Integer)MULTIPLIERS.get (unit)).intValue();
1245 delayTime *= multiplier;
1246 } else {
1247 throw new MessagingException("Unknown unit: "+unit);
1248 }
1249 }
1250
1251 /***
1252 * This constructor makes a default Delay object, ie. attempts=1 and delayTime=DEFAULT_DELAY_TIME
1253 **/
1254 public Delay () {
1255 }
1256
1257 /***
1258 * @return the delayTime for this Delay
1259 **/
1260 public long getDelayTime () {
1261 return delayTime;
1262 }
1263
1264 /***
1265 * @return the number attempts this Delay should be used.
1266 **/
1267 public int getAttempts () {
1268 return attempts;
1269 }
1270
1271 /***
1272 * Set the number attempts this Delay should be used.
1273 **/
1274 public void setAttempts (int value) {
1275 attempts = value;
1276 }
1277
1278 /***
1279 * Pretty prints this Delay
1280 **/
1281 public String toString () {
1282 StringBuffer buf = new StringBuffer(15);
1283 buf.append (getAttempts ());
1284 buf.append ('*');
1285 buf.append (getDelayTime());
1286 buf.append ("msec");
1287 return buf.toString();
1288 }
1289 }
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306 private Iterator getGatewaySMTPHostAddresses(final Collection gatewayServers) {
1307 return new Iterator() {
1308 private Iterator gateways = gatewayServers.iterator();
1309 private Iterator addresses = null;
1310
1311 public boolean hasNext() {
1312
1313
1314
1315
1316
1317
1318
1319 if (!hasNextAddress() && gateways.hasNext()) {
1320 do {
1321 String server = (String) gateways.next();
1322 String port = "25";
1323
1324 int idx = server.indexOf(':');
1325 if ( idx > 0) {
1326 port = server.substring(idx+1);
1327 server = server.substring(0,idx);
1328 }
1329
1330 final String nextGateway = server;
1331 final String nextGatewayPort = port;
1332 try {
1333 final InetAddress[] ips = org.apache.james.dnsserver.DNSServer.getAllByName(nextGateway);
1334 addresses = new Iterator() {
1335 private InetAddress[] ipAddresses = ips;
1336 int i = 0;
1337
1338 public boolean hasNext() {
1339 return i < ipAddresses.length;
1340 }
1341
1342 public Object next() {
1343 return new org.apache.mailet.HostAddress(nextGateway, "smtp://" + (ipAddresses[i++]).getHostAddress() + ":" + nextGatewayPort);
1344 }
1345
1346 public void remove() {
1347 throw new UnsupportedOperationException ("remove not supported by this iterator");
1348 }
1349 };
1350 }
1351 catch (java.net.UnknownHostException uhe) {
1352 log("Unknown gateway host: " + uhe.getMessage().trim());
1353 log("This could be a DNS server error or configuration error.");
1354 }
1355 } while (!hasNextAddress() && gateways.hasNext());
1356 }
1357
1358 return hasNextAddress();
1359 }
1360
1361 private boolean hasNextAddress() {
1362 return addresses != null && addresses.hasNext();
1363 }
1364
1365 public Object next() {
1366 return (addresses != null) ? addresses.next() : null;
1367 }
1368
1369 public void remove() {
1370 throw new UnsupportedOperationException ("remove not supported by this iterator");
1371 }
1372 };
1373 }
1374 }