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: 494056 $ $Date: 2007-01-08 13:15:03 +0000 (lun, 08 gen 2007) $
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 transport.close();
566 transport = null;
567 }
568 }
569 logMessageBuffer =
570 new StringBuffer(256)
571 .append("Mail (")
572 .append(mail.getName())
573 .append(") sent successfully to ")
574 .append(outgoingMailServer.getHostName())
575 .append(" at ")
576 .append(outgoingMailServer.getHost())
577 .append(" for ")
578 .append(mail.getRecipients());
579 log(logMessageBuffer.toString());
580 return true;
581 } catch (SendFailedException sfe) {
582 logSendFailedException(sfe);
583
584 if (sfe.getValidSentAddresses() != null) {
585 Address[] validSent = sfe.getValidSentAddresses();
586 if (validSent.length > 0) {
587 StringBuffer logMessageBuffer =
588 new StringBuffer(256)
589 .append("Mail (")
590 .append(mail.getName())
591 .append(") sent successfully for ")
592 .append(Arrays.asList(validSent));
593 log(logMessageBuffer.toString());
594 }
595 }
596
597
598 if (sfe instanceof SMTPSendFailedException) {
599 SMTPSendFailedException ssfe = (SMTPSendFailedException) sfe;
600
601 if (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599) throw sfe;
602 }
603
604 if (sfe.getValidUnsentAddresses() != null
605 && sfe.getValidUnsentAddresses().length > 0) {
606 if (isDebug) log("Send failed, " + sfe.getValidUnsentAddresses().length + " valid addresses remain, continuing with any other servers");
607 lastError = sfe;
608 continue;
609 } else {
610
611 throw sfe;
612 }
613 } catch (MessagingException me) {
614
615 StringBuffer exceptionBuffer =
616 new StringBuffer(256)
617 .append("Exception delivering message (")
618 .append(mail.getName())
619 .append(") - ")
620 .append(me.getMessage());
621 log(exceptionBuffer.toString());
622 if ((me.getNextException() != null) &&
623 (me.getNextException() instanceof java.io.IOException)) {
624
625
626
627
628 lastError = me;
629 continue;
630 }
631
632
633
634
635
636
637 throw me;
638 }
639 }
640
641
642
643
644
645
646 if (lastError != null) {
647 throw lastError;
648 }
649 } catch (SendFailedException sfe) {
650 logSendFailedException(sfe);
651
652 Collection recipients = mail.getRecipients();
653
654 boolean deleteMessage = false;
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677 if (sfe instanceof SMTPSendFailedException) {
678
679 SMTPSendFailedException ssfe = (SMTPSendFailedException) sfe;
680 deleteMessage = (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599);
681 } else {
682
683 MessagingException me = sfe;
684 Exception ne;
685 while ((ne = me.getNextException()) != null && ne instanceof MessagingException) {
686 me = (MessagingException)ne;
687 if (me instanceof SMTPAddressFailedException) {
688 SMTPAddressFailedException ssfe = (SMTPAddressFailedException)me;
689 deleteMessage = (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599);
690 }
691 }
692 }
693
694
695 if (isDebug) log("Recipients: " + recipients);
696
697 if (sfe.getInvalidAddresses() != null) {
698 Address[] address = sfe.getInvalidAddresses();
699 if (address.length > 0) {
700 recipients.clear();
701 for (int i = 0; i < address.length; i++) {
702 try {
703 recipients.add(new MailAddress(address[i].toString()));
704 } catch (ParseException pe) {
705
706
707
708 log("Can't parse invalid address: " + pe.getMessage());
709 }
710 }
711 if (isDebug) log("Invalid recipients: " + recipients);
712 deleteMessage = failMessage(mail, sfe, true);
713 }
714 }
715
716 if (sfe.getValidUnsentAddresses() != null) {
717 Address[] address = sfe.getValidUnsentAddresses();
718 if (address.length > 0) {
719 recipients.clear();
720 for (int i = 0; i < address.length; i++) {
721 try {
722 recipients.add(new MailAddress(address[i].toString()));
723 } catch (ParseException pe) {
724
725
726
727 log("Can't parse unsent address: " + pe.getMessage());
728 }
729 }
730 if (isDebug) log("Unsent recipients: " + recipients);
731 if (sfe instanceof SMTPSendFailedException) {
732 SMTPSendFailedException ssfe = (SMTPSendFailedException) sfe;
733 deleteMessage = failMessage(mail, sfe, ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599);
734 } else {
735 deleteMessage = failMessage(mail, sfe, false);
736 }
737 }
738 }
739
740 return deleteMessage;
741 } catch (MessagingException ex) {
742
743
744
745
746
747
748
749
750
751
752
753 return failMessage(mail, ex, ('5' == ex.getMessage().charAt(0)));
754 }
755
756
757
758
759
760
761
762 return failMessage(mail, new MessagingException("No mail server(s) available at this time."), false);
763 }
764
765 /***
766 * Converts a message to 7 bit.
767 *
768 * @param message
769 * @return
770 */
771 private void convertTo7Bit(MimePart part) throws MessagingException, IOException {
772 if (part.isMimeType("multipart/*")) {
773 MimeMultipart parts = (MimeMultipart) part.getContent();
774 int count = parts.getCount();
775 for (int i = 0; i < count; i++) {
776 convertTo7Bit((MimePart)parts.getBodyPart(i));
777 }
778 } else {
779 if (part.isMimeType("text/*")) {
780 part.setHeader("Content-Transfer-Encoding", "quoted-printable");
781 part.addHeader("X-MIME-Autoconverted", "from 8bit to quoted-printable by "+getMailetContext().getServerInfo());
782 } else {
783
784 part.setHeader("Content-Transfer-Encoding", "base64");
785 part.addHeader("X-MIME-Autoconverted", "from 8bit to base64 by "+getMailetContext().getServerInfo());
786 }
787 }
788 }
789
790 /***
791 * Adds an encoding information to each text mime part. This is a workaround
792 * for a javamail 1.3.2 bug: if a message is sent without encoding
793 * information a null pointer exception is thrown during the message
794 * delivery.
795 *
796 * @param part
797 * @throws MessagingException
798 * @throws IOException
799 */
800 private void setEncodingIfMissing(MimePart part) throws MessagingException, IOException {
801 if (part.isMimeType("text/*")) {
802 String enc = part.getEncoding();
803 if (enc == null) part.setHeader("Content-Transfer-Encoding", "7bit");
804 } else if (part.isMimeType("multipart/*")) {
805 Object content = part.getContent();
806 if (content instanceof MimeMultipart) {
807 MimeMultipart parts = (MimeMultipart) content;
808 int count = parts.getCount();
809 for (int i = 0; i < count; i++) {
810 setEncodingIfMissing((MimePart)parts.getBodyPart(i));
811 }
812 }
813 }
814 }
815
816 /***
817 * Insert the method's description here.
818 * Creation date: (2/25/00 1:14:18 AM)
819 * @param mail org.apache.james.core.MailImpl
820 * @param exception javax.mail.MessagingException
821 * @param boolean permanent
822 * @return boolean Whether the message failed fully and can be deleted
823 */
824 private boolean failMessage(Mail mail, MessagingException ex, boolean permanent) {
825 StringWriter sout = new StringWriter();
826 PrintWriter out = new PrintWriter(sout, true);
827 if (permanent) {
828 out.print("Permanent");
829 } else {
830 out.print("Temporary");
831 }
832 StringBuffer logBuffer =
833 new StringBuffer(64)
834 .append(" exception delivering mail (")
835 .append(mail.getName())
836 .append(": ");
837 out.print(logBuffer.toString());
838 if (isDebug) ex.printStackTrace(out);
839 log(sout.toString());
840 if (!permanent) {
841 if (!mail.getState().equals(Mail.ERROR)) {
842 mail.setState(Mail.ERROR);
843 mail.setErrorMessage("0");
844 mail.setLastUpdated(new Date());
845 }
846 int retries = Integer.parseInt(mail.getErrorMessage());
847 if (retries < maxRetries) {
848 logBuffer =
849 new StringBuffer(128)
850 .append("Storing message ")
851 .append(mail.getName())
852 .append(" into outgoing after ")
853 .append(retries)
854 .append(" retries");
855 log(logBuffer.toString());
856 ++retries;
857 mail.setErrorMessage(retries + "");
858 mail.setLastUpdated(new Date());
859 return false;
860 } else {
861 logBuffer =
862 new StringBuffer(128)
863 .append("Bouncing message ")
864 .append(mail.getName())
865 .append(" after ")
866 .append(retries)
867 .append(" retries");
868 log(logBuffer.toString());
869 }
870 }
871
872 if (mail.getSender() == null) {
873 log("Null Sender: no bounce will be generated for " + mail.getName());
874 return true;
875 }
876
877 if (bounceProcessor != null) {
878
879
880 mail.setAttribute("delivery-error", ex);
881 mail.setState(bounceProcessor);
882
883 MailetContext mc = getMailetContext();
884 try {
885 mc.sendMail(mail);
886 } catch (MessagingException e) {
887
888 log("Exception re-inserting failed mail: ", e);
889 }
890 } else {
891
892 bounce(mail, ex);
893 }
894 return true;
895 }
896
897 private void bounce(Mail mail, MessagingException ex) {
898 StringWriter sout = new StringWriter();
899 PrintWriter out = new PrintWriter(sout, true);
900 String machine = "[unknown]";
901 try {
902 InetAddress me = InetAddress.getLocalHost();
903 machine = me.getHostName();
904 } catch(Exception e){
905 machine = "[address unknown]";
906 }
907 StringBuffer bounceBuffer =
908 new StringBuffer(128)
909 .append("Hi. This is the James mail server at ")
910 .append(machine)
911 .append(".");
912 out.println(bounceBuffer.toString());
913 out.println("I'm afraid I wasn't able to deliver your message to the following addresses.");
914 out.println("This is a permanent error; I've given up. Sorry it didn't work out. Below");
915 out.println("I include the list of recipients and the reason why I was unable to deliver");
916 out.println("your message.");
917 out.println();
918 for (Iterator i = mail.getRecipients().iterator(); i.hasNext(); ) {
919 out.println(i.next());
920 }
921 if (ex.getNextException() == null) {
922 out.println(ex.getMessage().trim());
923 } else {
924 Exception ex1 = ex.getNextException();
925 if (ex1 instanceof SendFailedException) {
926 out.println("Remote mail server told me: " + ex1.getMessage().trim());
927 } else if (ex1 instanceof UnknownHostException) {
928 out.println("Unknown host: " + ex1.getMessage().trim());
929 out.println("This could be a DNS server error, a typo, or a problem with the recipient's mail server.");
930 } else if (ex1 instanceof ConnectException) {
931
932 out.println(ex1.getMessage().trim());
933 } else if (ex1 instanceof SocketException) {
934 out.println("Socket exception: " + ex1.getMessage().trim());
935 } else {
936 out.println(ex1.getMessage().trim());
937 }
938 }
939 out.println();
940
941 log("Sending failure message " + mail.getName());
942 try {
943 getMailetContext().bounce(mail, sout.toString());
944 } catch (MessagingException me) {
945 log("Encountered unexpected messaging exception while bouncing message: " + me.getMessage());
946 } catch (Exception e) {
947 log("Encountered unexpected exception while bouncing message: " + e.getMessage());
948 }
949 }
950
951 public String getMailetInfo() {
952 return "RemoteDelivery Mailet";
953 }
954
955 /***
956 * For this message, we take the list of recipients, organize these into distinct
957 * servers, and duplicate the message for each of these servers, and then call
958 * the deliver (messagecontainer) method for each server-specific
959 * messagecontainer ... that will handle storing it in the outgoing queue if needed.
960 *
961 * @param mail org.apache.mailet.Mail
962 */
963 public void service(Mail mail) throws MessagingException{
964
965 if (isDebug) {
966 log("Remotely delivering mail " + mail.getName());
967 }
968 Collection recipients = mail.getRecipients();
969
970 if (gatewayServer == null) {
971
972 Hashtable targets = new Hashtable();
973 for (Iterator i = recipients.iterator(); i.hasNext();) {
974 MailAddress target = (MailAddress)i.next();
975 String targetServer = target.getHost().toLowerCase(Locale.US);
976 Collection temp = (Collection)targets.get(targetServer);
977 if (temp == null) {
978 temp = new ArrayList();
979 targets.put(targetServer, temp);
980 }
981 temp.add(target);
982 }
983
984
985
986
987
988 String name = mail.getName();
989 for (Iterator i = targets.keySet().iterator(); i.hasNext(); ) {
990 String host = (String) i.next();
991 Collection rec = (Collection) targets.get(host);
992 if (isDebug) {
993 StringBuffer logMessageBuffer =
994 new StringBuffer(128)
995 .append("Sending mail to ")
996 .append(rec)
997 .append(" on host ")
998 .append(host);
999 log(logMessageBuffer.toString());
1000 }
1001 mail.setRecipients(rec);
1002 StringBuffer nameBuffer =
1003 new StringBuffer(128)
1004 .append(name)
1005 .append("-to-")
1006 .append(host);
1007 mail.setName(nameBuffer.toString());
1008 outgoing.store(mail);
1009
1010 }
1011 } else {
1012
1013 if (isDebug) {
1014 StringBuffer logMessageBuffer =
1015 new StringBuffer(128)
1016 .append("Sending mail to ")
1017 .append(mail.getRecipients())
1018 .append(" via ")
1019 .append(gatewayServer);
1020 log(logMessageBuffer.toString());
1021 }
1022
1023
1024 outgoing.store(mail);
1025 }
1026 mail.setState(Mail.GHOST);
1027 }
1028
1029
1030 public synchronized void destroy() {
1031
1032 destroyed = true;
1033
1034 for (Iterator i = deliveryThreads.iterator(); i.hasNext(); ) {
1035 Thread t = (Thread)i.next();
1036 t.interrupt();
1037 }
1038 notifyAll();
1039 }
1040
1041 /***
1042 * Handles checking the outgoing spool for new mail and delivering them if
1043 * there are any
1044 */
1045 public void run() {
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055 long stop = System.currentTimeMillis() + 60000;
1056 while ((getMailetContext().getAttribute(Constants.HELLO_NAME) == null)
1057 && stop > System.currentTimeMillis()) {
1058
1059 try {
1060 Thread.sleep(1000);
1061 } catch (Exception ignored) {}
1062 }
1063
1064
1065 Properties props = new Properties();
1066
1067 props.put("mail.debug", "false");
1068
1069
1070 props.put("mail.smtp.ehlo", "true");
1071
1072
1073
1074
1075 props.setProperty("mail.smtp.allow8bitmime", "false");
1076
1077 props.put("mail.smtp.timeout", smtpTimeout + "");
1078
1079 props.put("mail.smtp.connectiontimeout", connectionTimeout + "");
1080 props.put("mail.smtp.sendpartial",String.valueOf(sendPartial));
1081
1082
1083 if (getMailetContext().getAttribute(Constants.HELLO_NAME) != null) {
1084 props.put("mail.smtp.localhost", getMailetContext().getAttribute(Constants.HELLO_NAME));
1085 } else {
1086 String defaultDomain = (String) getMailetContext().getAttribute(Constants.DEFAULT_DOMAIN);
1087 if (defaultDomain != null) {
1088 props.put("mail.smtp.localhost", defaultDomain);
1089 }
1090 }
1091
1092 if (isBindUsed) {
1093
1094
1095 props.put("mail.smtp.socketFactory.class",
1096 "org.apache.james.transport.mailets.RemoteDeliverySocketFactory");
1097
1098 props.put("mail.smtp.socketFactory.fallback", "false");
1099 }
1100
1101 if (authUser != null) {
1102 props.put("mail.smtp.auth","true");
1103 }
1104
1105 props.putAll(defprops);
1106
1107 Session session = Session.getInstance(props, null);
1108 try {
1109 while (!Thread.interrupted() && !destroyed) {
1110 try {
1111 Mail mail = (Mail)outgoing.accept(delayFilter);
1112 String key = mail.getName();
1113 try {
1114 if (isDebug) {
1115 StringBuffer logMessageBuffer =
1116 new StringBuffer(128)
1117 .append(Thread.currentThread().getName())
1118 .append(" will process mail ")
1119 .append(key);
1120 log(logMessageBuffer.toString());
1121 }
1122 if (deliver(mail, session)) {
1123
1124 ContainerUtil.dispose(mail);
1125 outgoing.remove(key);
1126 } else {
1127
1128 outgoing.store(mail);
1129 ContainerUtil.dispose(mail);
1130
1131
1132 outgoing.unlock(key);
1133
1134
1135
1136
1137 }
1138
1139 mail = null;
1140 } catch (Exception e) {
1141
1142
1143
1144
1145
1146 ContainerUtil.dispose(mail);
1147 outgoing.remove(key);
1148 throw e;
1149 }
1150 } catch (Throwable e) {
1151 if (!destroyed) log("Exception caught in RemoteDelivery.run()", e);
1152 }
1153 }
1154 } finally {
1155
1156 Thread.interrupted();
1157 }
1158 }
1159
1160 /***
1161 * @param list holding Delay objects
1162 * @return the total attempts for all delays
1163 **/
1164 private int calcTotalAttempts (ArrayList list) {
1165 int sum = 0;
1166 Iterator i = list.iterator();
1167 while (i.hasNext()) {
1168 Delay delay = (Delay)i.next();
1169 sum += delay.getAttempts();
1170 }
1171 return sum;
1172 }
1173
1174 /***
1175 * This method expands an ArrayList containing Delay objects into an array holding the
1176 * only delaytime in the order.<p>
1177 * So if the list has 2 Delay objects the first having attempts=2 and delaytime 4000
1178 * the second having attempts=1 and delaytime=300000 will be expanded into this array:<p>
1179 * long[0] = 4000<p>
1180 * long[1] = 4000<p>
1181 * long[2] = 300000<p>
1182 * @param list the list to expand
1183 * @return the expanded list
1184 **/
1185 private long[] expandDelays (ArrayList list) {
1186 long[] delays = new long [calcTotalAttempts(list)];
1187 Iterator i = list.iterator();
1188 int idx = 0;
1189 while (i.hasNext()) {
1190 Delay delay = (Delay)i.next();
1191 for (int j=0; j<delay.getAttempts(); j++) {
1192 delays[idx++]= delay.getDelayTime();
1193 }
1194 }
1195 return delays;
1196 }
1197
1198 /***
1199 * This method returns, given a retry-count, the next delay time to use.
1200 * @param retry_count the current retry_count.
1201 * @return the next delay time to use, given the retry count
1202 **/
1203 private long getNextDelay (int retry_count) {
1204 if (retry_count > delayTimes.length) {
1205 return DEFAULT_DELAY_TIME;
1206 }
1207 return delayTimes[retry_count-1];
1208 }
1209
1210 /***
1211 * This class is used to hold a delay time and its corresponding number
1212 * of retries.
1213 **/
1214 private class Delay {
1215 private int attempts = 1;
1216 private long delayTime = DEFAULT_DELAY_TIME;
1217
1218
1219 /***
1220 * This constructor expects Strings of the form "[attempt\*]delaytime[unit]". <p>
1221 * The optional attempt is the number of tries this delay should be used (default = 1)
1222 * The unit if present must be one of (msec,sec,minute,hour,day) (default = msec)
1223 * The constructor multiplies the delaytime by the relevant multiplier for the unit,
1224 * so the delayTime instance variable is always in msec.
1225 * @param init_string the string to initialize this Delay object from
1226 **/
1227 public Delay (String init_string) throws MessagingException
1228 {
1229 String unit = "msec";
1230 if (delayTimeMatcher.matches (init_string, PATTERN)) {
1231 MatchResult res = delayTimeMatcher.getMatch ();
1232
1233
1234
1235
1236
1237 if (res.group(1) != null && !res.group(1).equals ("")) {
1238
1239 String attempt_match = res.group(1);
1240
1241 attempt_match = attempt_match.substring (0,attempt_match.length()-1).trim();
1242 attempts = Integer.parseInt (attempt_match);
1243 }
1244
1245 delayTime = Long.parseLong (res.group(2));
1246
1247 if (!res.group(3).equals ("")) {
1248
1249 unit = res.group(3).toLowerCase(Locale.US);
1250 }
1251 } else {
1252 throw new MessagingException(init_string+" does not match "+PATTERN_STRING);
1253 }
1254 if (MULTIPLIERS.get (unit)!=null) {
1255 int multiplier = ((Integer)MULTIPLIERS.get (unit)).intValue();
1256 delayTime *= multiplier;
1257 } else {
1258 throw new MessagingException("Unknown unit: "+unit);
1259 }
1260 }
1261
1262 /***
1263 * This constructor makes a default Delay object, ie. attempts=1 and delayTime=DEFAULT_DELAY_TIME
1264 **/
1265 public Delay () {
1266 }
1267
1268 /***
1269 * @return the delayTime for this Delay
1270 **/
1271 public long getDelayTime () {
1272 return delayTime;
1273 }
1274
1275 /***
1276 * @return the number attempts this Delay should be used.
1277 **/
1278 public int getAttempts () {
1279 return attempts;
1280 }
1281
1282 /***
1283 * Set the number attempts this Delay should be used.
1284 **/
1285 public void setAttempts (int value) {
1286 attempts = value;
1287 }
1288
1289 /***
1290 * Pretty prints this Delay
1291 **/
1292 public String toString () {
1293 StringBuffer buf = new StringBuffer(15);
1294 buf.append (getAttempts ());
1295 buf.append ('*');
1296 buf.append (getDelayTime());
1297 buf.append ("msec");
1298 return buf.toString();
1299 }
1300 }
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317 private Iterator getGatewaySMTPHostAddresses(final Collection gatewayServers) {
1318 return new Iterator() {
1319 private Iterator gateways = gatewayServers.iterator();
1320 private Iterator addresses = null;
1321
1322 public boolean hasNext() {
1323
1324
1325
1326
1327
1328
1329
1330 if (!hasNextAddress() && gateways.hasNext()) {
1331 do {
1332 String server = (String) gateways.next();
1333 String port = "25";
1334
1335 int idx = server.indexOf(':');
1336 if ( idx > 0) {
1337 port = server.substring(idx+1);
1338 server = server.substring(0,idx);
1339 }
1340
1341 final String nextGateway = server;
1342 final String nextGatewayPort = port;
1343 try {
1344 final InetAddress[] ips = org.apache.james.dnsserver.DNSServer.getAllByName(nextGateway);
1345 addresses = new Iterator() {
1346 private InetAddress[] ipAddresses = ips;
1347 int i = 0;
1348
1349 public boolean hasNext() {
1350 return i < ipAddresses.length;
1351 }
1352
1353 public Object next() {
1354 return new org.apache.mailet.HostAddress(nextGateway, "smtp://" + (ipAddresses[i++]).getHostAddress() + ":" + nextGatewayPort);
1355 }
1356
1357 public void remove() {
1358 throw new UnsupportedOperationException ("remove not supported by this iterator");
1359 }
1360 };
1361 }
1362 catch (java.net.UnknownHostException uhe) {
1363 log("Unknown gateway host: " + uhe.getMessage().trim());
1364 log("This could be a DNS server error or configuration error.");
1365 }
1366 } while (!hasNextAddress() && gateways.hasNext());
1367 }
1368
1369 return hasNextAddress();
1370 }
1371
1372 private boolean hasNextAddress() {
1373 return addresses != null && addresses.hasNext();
1374 }
1375
1376 public Object next() {
1377 return (addresses != null) ? addresses.next() : null;
1378 }
1379
1380 public void remove() {
1381 throw new UnsupportedOperationException ("remove not supported by this iterator");
1382 }
1383 };
1384 }
1385 }