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