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
21
22 package org.apache.james.transport.mailets;
23
24 import org.apache.avalon.cornerstone.services.store.Store;
25 import org.apache.avalon.framework.configuration.DefaultConfiguration;
26 import org.apache.avalon.framework.container.ContainerUtil;
27 import org.apache.avalon.framework.service.ServiceException;
28 import org.apache.avalon.framework.service.ServiceManager;
29 import org.apache.james.Constants;
30 import org.apache.james.api.dnsservice.DNSService;
31 import org.apache.james.api.dnsservice.TemporaryResolutionException;
32 import org.apache.james.services.SpoolRepository;
33 import org.apache.james.util.TimeConverter;
34 import org.apache.mailet.base.GenericMailet;
35 import org.apache.mailet.HostAddress;
36 import org.apache.mailet.Mail;
37 import org.apache.mailet.MailAddress;
38 import org.apache.mailet.MailetContext;
39 import org.apache.oro.text.regex.MalformedPatternException;
40 import org.apache.oro.text.regex.MatchResult;
41 import org.apache.oro.text.regex.Pattern;
42 import org.apache.oro.text.regex.Perl5Compiler;
43 import org.apache.oro.text.regex.Perl5Matcher;
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.lang.reflect.InvocationTargetException;
60 import java.lang.reflect.Method;
61 import java.net.ConnectException;
62 import java.net.InetAddress;
63 import java.net.SocketException;
64 import java.net.UnknownHostException;
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.Collection;
68 import java.util.Date;
69 import java.util.Hashtable;
70 import java.util.Iterator;
71 import java.util.Locale;
72 import java.util.Properties;
73 import java.util.StringTokenizer;
74 import java.util.Vector;
75
76
77 /**
78 * <p>Receives a MessageContainer from JamesSpoolManager and takes care of delivery
79 * the message to remote hosts. If for some reason mail can't be delivered
80 * store it in the "outgoing" Repository and set an Alarm. After the next "delayTime" the
81 * Alarm will wake the servlet that will try to send it again. After "maxRetries"
82 * the mail will be considered undeliverable and will be returned to sender.
83 * </p><p>
84 * TO DO (in priority):
85 * </p>
86 * <ol>
87 * <li>Support a gateway (a single server where all mail will be delivered) (DONE)</li>
88 * <li>Provide better failure messages (DONE)</li>
89 * <li>More efficiently handle numerous recipients</li>
90 * <li>Migrate to use Phoenix for the delivery threads</li>
91 * </ol>
92 * <p>
93 * You really want to read the JavaMail documentation if you are
94 * working in here, and you will want to view the list of JavaMail
95 * attributes, which are documented
96 * <a href='http://java.sun.com/products/javamail/javadocs/com/sun/mail/smtp/package-summary.html''>here</a>
97 * as well as other places.</p>
98 *
99 * @version CVS $Revision: 767520 $ $Date: 2009-04-22 14:38:29 +0100 (Wed, 22 Apr 2009) $
100 */
101 public class RemoteDelivery extends GenericMailet implements Runnable {
102
103 /** Default Delay Time (Default is 6*60*60*1000 Milliseconds (6 hours)). */
104 private static final long DEFAULT_DELAY_TIME = 21600000;
105
106 /** Pattern to match [attempts*]delay[units]. */
107 private static final String PATTERN_STRING = "\\s*([0-9]*\\s*[\\*])?\\s*([0-9]+)\\s*([a-z,A-Z]*)\\s*";
108
109 /** Compiled pattern of the above String. */
110 private static Pattern PATTERN = null;
111
112 /** The DNSService */
113 private DNSService dnsServer;
114
115 /**
116 * Static initializer.<p>
117 * Compiles pattern for processing delaytime entries.<p>
118 * Initializes MULTIPLIERS with the supported unit quantifiers
119 */
120 static {
121 try {
122 Perl5Compiler compiler = new Perl5Compiler();
123 PATTERN = compiler.compile(PATTERN_STRING, Perl5Compiler.READ_ONLY_MASK);
124 } catch(MalformedPatternException mpe) {
125 //this should not happen as the pattern string is hardcoded.
126 mpe.printStackTrace (System.err);
127 }
128 }
129
130 /**
131 * This filter is used in the accept call to the spool.
132 * It will select the next mail ready for processing according to the mails
133 * retrycount and lastUpdated time
134 **/
135 private class MultipleDelayFilter implements SpoolRepository.AcceptFilter {
136 /**
137 * holds the time to wait for the youngest mail to get ready for processing
138 **/
139 long youngest = 0;
140
141 /**
142 * Uses the getNextDelay to determine if a mail is ready for processing based on the delivered parameters
143 * errorMessage (which holds the retrycount), lastUpdated and state
144 * @param key the name/key of the message
145 * @param state the mails state
146 * @param lastUpdated the mail was last written to the spool at this time.
147 * @param errorMessage actually holds the retrycount as a string (see failMessage below)
148 **/
149 public boolean accept (String key, String state, long lastUpdated, String errorMessage) {
150 if (state.equals(Mail.ERROR)) {
151 //Test the time...
152 int retries = 0;
153
154 try {
155 retries = Integer.parseInt(errorMessage);
156 } catch (NumberFormatException e) {
157 // Something strange was happen with the errorMessage.. Ignore the Exception and try
158 // to deliver it now!
159 }
160
161 // If the retries count is 0 we should try to send the mail now!
162 if (retries == 0) return true;
163
164 long delay = getNextDelay (retries);
165 long timeToProcess = delay + lastUpdated;
166
167
168 if (System.currentTimeMillis() > timeToProcess) {
169 //We're ready to process this again
170 return true;
171 } else {
172 //We're not ready to process this.
173 if (youngest == 0 || youngest > timeToProcess) {
174 //Mark this as the next most likely possible mail to process
175 youngest = timeToProcess;
176 }
177 return false;
178 }
179 } else {
180 //This mail is good to go... return the key
181 return true;
182 }
183 }
184
185 /**
186 * @return the optimal time the SpoolRepository.accept(AcceptFilter) method should wait before
187 * trying to find a mail ready for processing again.
188 **/
189 public long getWaitTime () {
190 if (youngest == 0) {
191 return 0;
192 } else {
193 long duration = youngest - System.currentTimeMillis();
194 youngest = 0; //get ready for next run
195 return duration <= 0 ? 1 : duration;
196 }
197 }
198 }
199
200 /** Flag to define verbose logging messages. */
201 private boolean isDebug = false;
202
203 /** Repository used to store messages that will be delivered. */
204 private SpoolRepository workRepository;
205
206 /** List of Delay Times. Controls frequency of retry attempts. */
207 private long[] delayTimes;
208
209 /** Maximum no. of retries (Defaults to 5). */
210 private int maxRetries = 5;
211
212 /** Default number of ms to timeout on smtp delivery */
213 private long smtpTimeout = 180000;
214
215 /** If false then ANY address errors will cause the transmission to fail */
216 private boolean sendPartial = false;
217
218 /** The amount of time JavaMail will wait before giving up on a socket connect() */
219 private int connectionTimeout = 60000;
220
221 /** No. of threads used to process messages that should be retried. */
222 private int workersThreadCount = 1;
223
224 /** The server(s) to send all email to */
225 private Collection gatewayServer = null;
226
227 /** Auth for gateway server */
228 private String authUser = null;
229
230 /** Password for gateway server */
231 private String authPass = null;
232
233 /**
234 * JavaMail delivery socket binds to this local address.
235 * If null the JavaMail default will be used.
236 */
237 private String bindAddress = null;
238
239 /**
240 * True, if the bind configuration parameter is supplied, RemoteDeliverySocketFactory
241 * will be used in this case.
242 */
243 private boolean isBindUsed = false;
244
245 /** Collection that stores all worker threads.*/
246 private Collection workersThreads = new Vector();
247
248 /** Flag used by 'run' method to end itself. */
249 private volatile boolean destroyed = false;
250
251 /** the processor for creating Bounces */
252 private String bounceProcessor = null;
253
254 /**
255 * Matcher used in 'init' method to parse delayTimes specified in config
256 * file.
257 */
258 private Perl5Matcher delayTimeMatcher;
259
260 /**
261 * Filter used by 'accept' to check if message is ready for retrying.
262 */
263 private MultipleDelayFilter delayFilter = new MultipleDelayFilter();
264
265 /** Default properties for the JavaMail Session */
266 private Properties defprops = new Properties();
267
268 /** The retry count dnsProblemErrors */
269 private int dnsProblemRetry = 0;
270
271 /**
272 * Initializes all arguments based on configuration values specified in the
273 * James configuration file.
274 *
275 * @throws MessagingException
276 * on failure to initialize attributes.
277 */
278 public void init() throws MessagingException {
279 // Set isDebug flag.
280 isDebug = (getInitParameter("debug") == null) ? false : new Boolean(getInitParameter("debug")).booleanValue();
281
282 // Create list of Delay Times.
283 ArrayList delayTimesList = new ArrayList();
284 try {
285 if (getInitParameter("delayTime") != null) {
286 delayTimeMatcher = new Perl5Matcher();
287 String delayTimesParm = getInitParameter("delayTime");
288
289 // Split on commas
290 StringTokenizer st = new StringTokenizer (delayTimesParm,",");
291 while (st.hasMoreTokens()) {
292 String delayTime = st.nextToken();
293 delayTimesList.add (new Delay(delayTime));
294 }
295 } else {
296 // Use default delayTime.
297 delayTimesList.add (new Delay());
298 }
299 } catch (Exception e) {
300 log("Invalid delayTime setting: " + getInitParameter("delayTime"));
301 }
302
303 try {
304 // Get No. of Max Retries.
305 if (getInitParameter("maxRetries") != null) {
306 maxRetries = Integer.parseInt(getInitParameter("maxRetries"));
307 }
308
309 // Check consistency of 'maxRetries' with delayTimesList attempts.
310 int totalAttempts = calcTotalAttempts(delayTimesList);
311
312 // If inconsistency found, fix it.
313 if (totalAttempts > maxRetries) {
314 log("Total number of delayTime attempts exceeds maxRetries specified. "
315 + " Increasing maxRetries from "
316 + maxRetries
317 + " to "
318 + totalAttempts);
319 maxRetries = totalAttempts;
320 } else {
321 int extra = maxRetries - totalAttempts;
322 if (extra != 0) {
323 log("maxRetries is larger than total number of attempts specified. "
324 + "Increasing last delayTime with "
325 + extra
326 + " attempts ");
327
328 // Add extra attempts to the last delayTime.
329 if (delayTimesList.size() != 0) {
330 // Get the last delayTime.
331 Delay delay = (Delay) delayTimesList.get(delayTimesList
332 .size() - 1);
333
334 // Increase no. of attempts.
335 delay.setAttempts(delay.getAttempts() + extra);
336 log("Delay of " + delay.getDelayTime()
337 + " msecs is now attempted: " + delay.getAttempts()
338 + " times");
339 } else {
340 throw new MessagingException(
341 "No delaytimes, cannot continue");
342 }
343 }
344 }
345 delayTimes = expandDelays(delayTimesList);
346
347 } catch (Exception e) {
348 log("Invalid maxRetries setting: " + getInitParameter("maxRetries"));
349 }
350
351 ServiceManager compMgr = (ServiceManager) getMailetContext()
352 .getAttribute(Constants.AVALON_COMPONENT_MANAGER);
353
354 // Get the path for the 'Outgoing' repository. This is the place on the
355 // file system where Mail objects will be saved during the 'delivery'
356 // processing. This can be changed to a repository on a database (e.g.
357 // db://maildb/spool/retry).
358 String workRepositoryPath = getInitParameter("outgoing");
359 if (workRepositoryPath == null) {
360 workRepositoryPath = "file:///../var/mail/outgoing";
361 }
362
363 try {
364 // Instantiate a MailRepository for mails to be processed.
365 Store mailstore = (Store) compMgr.lookup(Store.ROLE);
366
367 DefaultConfiguration spoolConf = new DefaultConfiguration(
368 "repository", "generated:RemoteDelivery");
369 spoolConf.setAttribute("destinationURL", workRepositoryPath);
370 spoolConf.setAttribute("type", "SPOOL");
371 workRepository = (SpoolRepository) mailstore.select(spoolConf);
372 } catch (ServiceException cnfe) {
373 log("Failed to retrieve Store component:" + cnfe.getMessage());
374 throw new MessagingException("Failed to retrieve Store component",
375 cnfe);
376 }
377
378 // Start Workers Threads.
379 workersThreadCount = Integer.parseInt(getInitParameter("deliveryThreads"));
380 for (int i = 0; i < workersThreadCount; i++) {
381 String threadName = "Remote delivery thread (" + i + ")";
382 Thread t = new Thread(this, threadName);
383 t.start();
384 workersThreads.add(t);
385 }
386
387 try {
388 if (getInitParameter("timeout") != null) {
389 smtpTimeout = Integer.parseInt(getInitParameter("timeout"));
390 }
391 } catch (Exception e) {
392 log("Invalid timeout setting: " + getInitParameter("timeout"));
393 }
394
395 try {
396 if (getInitParameter("connectiontimeout") != null) {
397 connectionTimeout = Integer.parseInt(getInitParameter("connectiontimeout"));
398 }
399 } catch (Exception e) {
400 log("Invalid timeout setting: " + getInitParameter("timeout"));
401 }
402
403 sendPartial = (getInitParameter("sendpartial") == null) ? false : new Boolean(getInitParameter("sendpartial")).booleanValue();
404
405 bounceProcessor = getInitParameter("bounceProcessor");
406
407 String gateway = getInitParameter("gateway");
408 String gatewayPort = getInitParameter("gatewayPort");
409
410 if (gateway != null) {
411 gatewayServer = new ArrayList();
412 StringTokenizer st = new StringTokenizer(gateway, ",") ;
413 while (st.hasMoreTokens()) {
414 String server = st.nextToken().trim() ;
415 if (server.indexOf(':') < 0 && gatewayPort != null) {
416 server += ":";
417 server += gatewayPort;
418 }
419
420 if (isDebug) log("Adding SMTP gateway: " + server) ;
421 gatewayServer.add(server);
422 }
423 authUser = getInitParameter("gatewayUsername");
424 // backward compatibility with 2.3.x
425 if (authUser == null) {
426 authUser = getInitParameter("gatewayusername");
427 }
428 authPass = getInitParameter("gatewayPassword");
429 }
430
431 // Instantiate the DNSService for DNS queries lookup
432 try {
433 dnsServer = (DNSService) compMgr.lookup(DNSService.ROLE);
434 } catch (ServiceException e1) {
435 log("Failed to retrieve DNSService" + e1.getMessage());
436 }
437
438 bindAddress = getInitParameter("bind");
439 isBindUsed = bindAddress != null;
440 try {
441 if (isBindUsed) RemoteDeliverySocketFactory.setBindAdress(bindAddress);
442 } catch (UnknownHostException e) {
443 log("Invalid bind setting (" + bindAddress + "): " + e.toString());
444 }
445
446 // deal with <mail.*> attributes, passing them to javamail
447 Iterator i = getInitParameterNames();
448 while (i.hasNext()) {
449 String name = (String) i.next();
450 if (name.startsWith("mail.")) {
451 defprops.put(name,getInitParameter(name));
452 }
453
454 }
455
456 String dnsRetry = getInitParameter("maxDnsProblemRetries");
457 if (dnsRetry != null && !dnsRetry.equals("")) {
458 dnsProblemRetry = Integer.parseInt(dnsRetry);
459 }
460 }
461
462 /**
463 * Calculates Total no. of attempts for the specified delayList.
464 *
465 * @param delayList
466 * list of 'Delay' objects
467 * @return total no. of retry attempts
468 */
469 private int calcTotalAttempts (ArrayList delayList) {
470 int sum = 0;
471 Iterator i = delayList.iterator();
472 while (i.hasNext()) {
473 Delay delay = (Delay)i.next();
474 sum += delay.getAttempts();
475 }
476 return sum;
477 }
478
479 /**
480 * This method expands an ArrayList containing Delay objects into an array holding the
481 * only delaytime in the order.<p>
482 * So if the list has 2 Delay objects the first having attempts=2 and delaytime 4000
483 * the second having attempts=1 and delaytime=300000 will be expanded into this array:<p>
484 * long[0] = 4000<p>
485 * long[1] = 4000<p>
486 * long[2] = 300000<p>
487 * @param list the list to expand
488 * @return the expanded list
489 **/
490 private long[] expandDelays (ArrayList list) {
491 long[] delays = new long [calcTotalAttempts(list)];
492 Iterator i = list.iterator();
493 int idx = 0;
494 while (i.hasNext()) {
495 Delay delay = (Delay)i.next();
496 for (int j=0; j<delay.getAttempts(); j++) {
497 delays[idx++]= delay.getDelayTime();
498 }
499 }
500 return delays;
501 }
502
503 /**
504 * This method returns, given a retry-count, the next delay time to use.
505 * @param retry_count the current retry_count.
506 * @return the next delay time to use, given the retry count
507 **/
508 private long getNextDelay (int retry_count) {
509 if (retry_count > delayTimes.length) {
510 return DEFAULT_DELAY_TIME;
511 }
512 return delayTimes[retry_count-1];
513 }
514
515 /**
516 * This class is used to hold a delay time and its corresponding number of
517 * retries.
518 **/
519 private class Delay {
520 private int attempts = 1;
521
522 private long delayTime = DEFAULT_DELAY_TIME;
523
524 /**
525 * This constructor expects Strings of the form
526 * "[attempt\*]delaytime[unit]".
527 * <p>
528 * The optional attempt is the number of tries this delay should be used
529 * (default = 1). The unit, if present, must be one of
530 * (msec,sec,minute,hour,day). The default value of unit is 'msec'.
531 * <p>
532 * The constructor multiplies the delaytime by the relevant multiplier
533 * for the unit, so the delayTime instance variable is always in msec.
534 *
535 * @param initString
536 * the string to initialize this Delay object from
537 **/
538 public Delay(String initString) throws MessagingException {
539 // Default unit value to 'msec'.
540 String unit = "msec";
541
542 if (delayTimeMatcher.matches(initString, PATTERN)) {
543 MatchResult res = delayTimeMatcher.getMatch();
544
545 // The capturing groups will now hold:
546 // at 1: attempts * (if present)
547 // at 2: delaytime
548 // at 3: unit (if present)
549 if (res.group(1) != null && !res.group(1).equals("")) {
550 // We have an attempt *
551 String attemptMatch = res.group(1);
552
553 // Strip the * and whitespace.
554 attemptMatch = attemptMatch.substring(0,
555 attemptMatch.length() - 1).trim();
556 attempts = Integer.parseInt(attemptMatch);
557 }
558
559 delayTime = Long.parseLong(res.group(2));
560
561 if (!res.group(3).equals("")) {
562 // We have a value for 'unit'.
563 unit = res.group(3).toLowerCase(Locale.US);
564 }
565 } else {
566 throw new MessagingException(initString + " does not match "
567 + PATTERN_STRING);
568 }
569
570 // calculate delayTime.
571 try {
572 delayTime = TimeConverter.getMilliSeconds(delayTime, unit);
573 } catch (NumberFormatException e) {
574 throw new MessagingException(e.getMessage());
575 }
576 }
577
578 /**
579 * This constructor makes a default Delay object with attempts = 1 and
580 * delayTime = DEFAULT_DELAY_TIME.
581 **/
582 public Delay() {
583 }
584
585 /**
586 * @return the delayTime for this Delay
587 **/
588 public long getDelayTime() {
589 return delayTime;
590 }
591
592 /**
593 * @return the number attempts this Delay should be used.
594 **/
595 public int getAttempts() {
596 return attempts;
597 }
598
599 /**
600 * Set the number attempts this Delay should be used.
601 **/
602 public void setAttempts(int value) {
603 attempts = value;
604 }
605
606 /**
607 * Pretty prints this Delay
608 **/
609 public String toString() {
610 String message = getAttempts() + "*" + getDelayTime() + "msecs";
611 return message;
612 }
613 }
614
615 public String getMailetInfo() {
616 return "RemoteDelivery Mailet";
617 }
618
619 /**
620 * For this message, we take the list of recipients, organize these into distinct
621 * servers, and duplicate the message for each of these servers, and then call
622 * the deliver (messagecontainer) method for each server-specific
623 * messagecontainer ... that will handle storing it in the outgoing queue if needed.
624 *
625 * @param mail org.apache.mailet.Mail
626 */
627 public void service(Mail mail) throws MessagingException{
628 // Do I want to give the internal key, or the message's Message ID
629 if (isDebug) {
630 log("Remotely delivering mail " + mail.getName());
631 }
632 Collection recipients = mail.getRecipients();
633
634 if (gatewayServer == null) {
635 // Must first organize the recipients into distinct servers (name made case insensitive)
636 Hashtable targets = new Hashtable();
637 for (Iterator i = recipients.iterator(); i.hasNext();) {
638 MailAddress target = (MailAddress)i.next();
639 String targetServer = target.getHost().toLowerCase(Locale.US);
640 Collection temp = (Collection)targets.get(targetServer);
641 if (temp == null) {
642 temp = new ArrayList();
643 targets.put(targetServer, temp);
644 }
645 temp.add(target);
646 }
647
648 //We have the recipients organized into distinct servers... put them into the
649 //delivery store organized like this... this is ultra inefficient I think...
650
651 // Store the new message containers, organized by server, in the outgoing mail repository
652 String name = mail.getName();
653 for (Iterator i = targets.keySet().iterator(); i.hasNext(); ) {
654 String host = (String) i.next();
655 Collection rec = (Collection) targets.get(host);
656 if (isDebug) {
657 StringBuffer logMessageBuffer =
658 new StringBuffer(128)
659 .append("Sending mail to ")
660 .append(rec)
661 .append(" on host ")
662 .append(host);
663 log(logMessageBuffer.toString());
664 }
665 mail.setRecipients(rec);
666 StringBuffer nameBuffer =
667 new StringBuffer(128)
668 .append(name)
669 .append("-to-")
670 .append(host);
671 mail.setName(nameBuffer.toString());
672 workRepository.store(mail);
673 //Set it to try to deliver (in a separate thread) immediately (triggered by storage)
674 }
675 } else {
676 // Store the mail unaltered for processing by the gateway server(s)
677 if (isDebug) {
678 StringBuffer logMessageBuffer =
679 new StringBuffer(128)
680 .append("Sending mail to ")
681 .append(mail.getRecipients())
682 .append(" via ")
683 .append(gatewayServer);
684 log(logMessageBuffer.toString());
685 }
686
687 //Set it to try to deliver (in a separate thread) immediately (triggered by storage)
688 workRepository.store(mail);
689 }
690 mail.setState(Mail.GHOST);
691 }
692
693 /**
694 * Stops all the worker threads that are waiting for messages. This method is
695 * called by the Mailet container before taking this Mailet out of service.
696 */
697 public synchronized void destroy() {
698 // Mark flag so threads from this Mailet stop themselves
699 destroyed = true;
700
701 // Wake up all threads from waiting for an accept
702 for (Iterator i = workersThreads.iterator(); i.hasNext(); ) {
703 Thread t = (Thread)i.next();
704 t.interrupt();
705 }
706 notifyAll();
707 }
708
709
710 /**
711 * Handles checking the outgoing spool for new mail and delivering them if
712 * there are any
713 */
714 public void run() {
715
716 /* TODO: CHANGE ME!!! The problem is that we need to wait for James to
717 * finish initializing. We expect the HELLO_NAME to be put into
718 * the MailetContext, but in the current configuration we get
719 * started before the SMTP Server, which establishes the value.
720 * Since there is no contractual guarantee that there will be a
721 * HELLO_NAME value, we can't just wait for it. As a temporary
722 * measure, I'm inserting this philosophically unsatisfactory
723 * fix.
724 */
725 long stop = System.currentTimeMillis() + 60000;
726 while ((getMailetContext().getAttribute(Constants.HELLO_NAME) == null)
727 && stop > System.currentTimeMillis()) {
728 try {
729 Thread.sleep(1000);
730 } catch (Exception ignored) {} // wait for James to finish initializing
731 }
732
733 //Checks the pool and delivers a mail message
734 Properties props = new Properties();
735 //Not needed for production environment
736 props.put("mail.debug", "false");
737 // Reactivated: javamail 1.3.2 should no more have problems with "250 OK"
738 // messages (WAS "false": Prevents problems encountered with 250 OK Messages)
739 props.put("mail.smtp.ehlo", "true");
740 // By setting this property to true the transport is allowed to
741 // send 8 bit data to the server (if it supports the 8bitmime extension).
742 // 2006/03/01 reverted to false because of a javamail bug converting to 8bit
743 // messages created by an inputstream.
744 props.setProperty("mail.smtp.allow8bitmime", "true");
745 //Sets timeout on going connections
746 props.put("mail.smtp.timeout", smtpTimeout + "");
747
748 props.put("mail.smtp.connectiontimeout", connectionTimeout + "");
749 props.put("mail.smtp.sendpartial",String.valueOf(sendPartial));
750
751 //Set the hostname we'll use as this server
752 if (getMailetContext().getAttribute(Constants.HELLO_NAME) != null) {
753 props.put("mail.smtp.localhost", getMailetContext().getAttribute(Constants.HELLO_NAME));
754 }
755 else {
756 String defaultDomain = (String) getMailetContext().getAttribute(Constants.DEFAULT_DOMAIN);
757 if (defaultDomain != null) {
758 props.put("mail.smtp.localhost", defaultDomain);
759 }
760 }
761
762 if (isBindUsed) {
763 // undocumented JavaMail 1.2 feature, smtp transport will use
764 // our socket factory, which will also set the local address
765 props.put("mail.smtp.socketFactory.class", RemoteDeliverySocketFactory.class.getClass());
766 // Don't fallback to the standard socket factory on error, do throw an exception
767 props.put("mail.smtp.socketFactory.fallback", "false");
768 }
769
770 if (authUser != null) {
771 props.put("mail.smtp.auth","true");
772 }
773
774 props.putAll(defprops);
775
776 Session session = obtainSession(props);
777 try {
778 while (!Thread.interrupted() && !destroyed) {
779 try {
780 // Get the 'mail' object that is ready for deliverying. If no
781 // message is
782 // ready, the 'accept' will block until message is ready.
783 // The amount
784 // of time to block is determined by the 'getWaitTime'
785 // method of the
786 // MultipleDelayFilter.
787 Mail mail = workRepository.accept(delayFilter);
788 String key = mail.getName();
789 try {
790 if (isDebug) {
791 String message = Thread.currentThread().getName()
792 + " will process mail " + key;
793 log(message);
794 }
795
796 // Deliver message
797 if (deliver(mail, session)) {
798 // Message was successfully delivered/fully failed...
799 // delete it
800 ContainerUtil.dispose(mail);
801 workRepository.remove(key);
802 } else {
803 // Something happened that will delay delivery.
804 // Store it back in the retry repository.
805 workRepository.store(mail);
806 ContainerUtil.dispose(mail);
807
808 // This is an update, so we have to unlock and
809 // notify or this mail is kept locked by this thread.
810 workRepository.unlock(key);
811
812 // Note: We do not notify because we updated an
813 // already existing mail and we are now free to handle
814 // more mails.
815 // Furthermore this mail should not be processed now
816 // because we have a retry time scheduling.
817 }
818
819 // Clear the object handle to make sure it recycles
820 // this object.
821 mail = null;
822 } catch (Exception e) {
823 // Prevent unexpected exceptions from causing looping by
824 // removing message from outgoing.
825 // DO NOT CHANGE THIS to catch Error! For example, if
826 // there were an OutOfMemory condition caused because
827 // something else in the server was abusing memory, we would
828 // not want to start purging the retrying spool!
829 ContainerUtil.dispose(mail);
830 workRepository.remove(key);
831 throw e;
832 }
833 } catch (Throwable e) {
834 if (!destroyed) {
835 log("Exception caught in RemoteDelivery.run()", e);
836 }
837 }
838 }
839 } finally {
840 // Restore the thread state to non-interrupted.
841 Thread.interrupted();
842 }
843 }
844
845
846 /**
847 * We can assume that the recipients of this message are all going to the same
848 * mail server. We will now rely on the DNS server to do DNS MX record lookup
849 * and try to deliver to the multiple mail servers. If it fails, it should
850 * throw an exception.
851 *
852 * Creation date: (2/24/00 11:25:00 PM)
853 * @param mail org.apache.james.core.MailImpl
854 * @param session javax.mail.Session
855 * @return boolean Whether the delivery was successful and the message can be deleted
856 */
857 private boolean deliver(Mail mail, Session session) {
858 try {
859 if (isDebug) {
860 log("Attempting to deliver " + mail.getName());
861 }
862 MimeMessage message = mail.getMessage();
863
864 //Create an array of the recipients as InternetAddress objects
865 Collection recipients = mail.getRecipients();
866 InternetAddress addr[] = new InternetAddress[recipients.size()];
867 int j = 0;
868 for (Iterator i = recipients.iterator(); i.hasNext(); j++) {
869 MailAddress rcpt = (MailAddress)i.next();
870 addr[j] = rcpt.toInternetAddress();
871 }
872
873 if (addr.length <= 0) {
874 log("No recipients specified... not sure how this could have happened.");
875 return true;
876 }
877
878 //Figure out which servers to try to send to. This collection
879 // will hold all the possible target servers
880 Iterator targetServers = null;
881 if (gatewayServer == null) {
882 MailAddress rcpt = (MailAddress) recipients.iterator().next();
883 String host = rcpt.getHost();
884
885 //Lookup the possible targets
886 try {
887 targetServers = dnsServer.getSMTPHostAddresses(host);
888 } catch (TemporaryResolutionException e) {
889 log("Temporary problem looking up mail server for host: " + host);
890 StringBuffer exceptionBuffer =
891 new StringBuffer(128)
892 .append("Temporary problem looking up mail server for host: ")
893 .append(host)
894 .append(". I cannot determine where to send this message.");
895
896 // temporary problems
897 return failMessage(mail, new MessagingException(exceptionBuffer.toString()), false);
898 }
899 if (!targetServers.hasNext()) {
900 log("No mail server found for: " + host);
901 StringBuffer exceptionBuffer =
902 new StringBuffer(128)
903 .append("There are no DNS entries for the hostname ")
904 .append(host)
905 .append(". I cannot determine where to send this message.");
906
907 int retry = 0;
908 try {
909 retry = Integer.parseInt(mail.getErrorMessage());
910 } catch (NumberFormatException e) {
911 // Unable to parse retryCount
912 }
913 if (retry == 0 || retry > dnsProblemRetry) {
914 // The domain has no dns entry.. Return a permanent error
915 return failMessage(mail, new MessagingException(exceptionBuffer.toString()), true);
916 } else {
917 return failMessage(mail, new MessagingException(exceptionBuffer.toString()), false);
918 }
919 }
920 } else {
921 targetServers = getGatewaySMTPHostAddresses(gatewayServer);
922 }
923
924 MessagingException lastError = null;
925
926 while ( targetServers.hasNext()) {
927 try {
928 HostAddress outgoingMailServer = (HostAddress) targetServers.next();
929 StringBuffer logMessageBuffer =
930 new StringBuffer(256)
931 .append("Attempting delivery of ")
932 .append(mail.getName())
933 .append(" to host ")
934 .append(outgoingMailServer.getHostName())
935 .append(" at ")
936 .append(outgoingMailServer.getHost())
937 .append(" for addresses ")
938 .append(Arrays.asList(addr));
939 log(logMessageBuffer.toString());
940
941 Properties props = session.getProperties();
942 if (mail.getSender() == null) {
943 props.put("mail.smtp.from", "<>");
944 } else {
945 String sender = mail.getSender().toString();
946 props.put("mail.smtp.from", sender);
947 }
948
949 //Many of these properties are only in later JavaMail versions
950 //"mail.smtp.ehlo" //default true
951 //"mail.smtp.auth" //default false
952 //"mail.smtp.dsn.ret" //default to nothing... appended as RET= after MAIL FROM line.
953 //"mail.smtp.dsn.notify" //default to nothing...appended as NOTIFY= after RCPT TO line.
954
955 Transport transport = null;
956 try {
957 transport = session.getTransport(outgoingMailServer);
958 try {
959 if (authUser != null) {
960 transport.connect(outgoingMailServer.getHostName(), authUser, authPass);
961 } else {
962 transport.connect();
963 }
964 } catch (MessagingException me) {
965 // Any error on connect should cause the mailet to attempt
966 // to connect to the next SMTP server associated with this
967 // MX record. Just log the exception. We'll worry about
968 // failing the message at the end of the loop.
969 log(me.getMessage());
970 continue;
971 }
972 // if the transport is a SMTPTransport (from sun) some
973 // performance enhancement can be done.
974 if (transport.getClass().getName().endsWith(".SMTPTransport")) {
975 boolean supports8bitmime = false;
976 try {
977 Method supportsExtension = transport.getClass().getMethod("supportsExtension", new Class[] {String.class});
978 supports8bitmime = ((Boolean) supportsExtension.invoke(transport, new Object[] {"8BITMIME"})).booleanValue();
979 } catch (NoSuchMethodException nsme) {
980 // An SMTPAddressFailedException with no getAddress method.
981 } catch (IllegalAccessException iae) {
982 } catch (IllegalArgumentException iae) {
983 } catch (InvocationTargetException ite) {
984 // Other issues with getAddress invokation.
985 }
986
987 // if the message is alredy 8bit or binary and the
988 // server doesn't support the 8bit extension it has
989 // to be converted to 7bit. Javamail api doesn't perform
990 // that conversion, but it is required to be a
991 // rfc-compliant smtp server.
992
993 // Temporarily disabled. See JAMES-638
994 if (!supports8bitmime) {
995 try {
996 convertTo7Bit(message);
997 } catch (IOException e) {
998 // An error has occured during the 7bit conversion.
999 // The error is logged and the message is sent anyway.
1000
1001 log("Error during the conversion to 7 bit.", e);
1002 }
1003 }
1004 } else {
1005 // If the transport is not the one
1006 // developed by Sun we are not sure of how it
1007 // handles the 8 bit mime stuff,
1008 // so I convert the message to 7bit.
1009 try {
1010 convertTo7Bit(message);
1011 } catch (IOException e) {
1012 log("Error during the conversion to 7 bit.", e);
1013 }
1014 }
1015 transport.sendMessage(message, addr);
1016 } finally {
1017 if (transport != null) {
1018 try
1019 {
1020 // James-899: transport.close() sends QUIT to the server; if that fails
1021 // (e.g. because the server has already closed the connection) the message
1022 // should be considered to be delivered because the error happened outside
1023 // of the mail transaction (MAIL, RCPT, DATA).
1024 transport.close();
1025 }
1026 catch (MessagingException e)
1027 {
1028 log("Warning: could not close the SMTP transport after sending mail (" + mail.getName()
1029 + ") to " + outgoingMailServer.getHostName() + " at " + outgoingMailServer.getHost()
1030 + " for " + mail.getRecipients() + "; probably the server has already closed the "
1031 + "connection. Message is considered to be delivered. Exception: " + e.getMessage());
1032 }
1033 transport = null;
1034 }
1035 }
1036 logMessageBuffer =
1037 new StringBuffer(256)
1038 .append("Mail (")
1039 .append(mail.getName())
1040 .append(") sent successfully to ")
1041 .append(outgoingMailServer.getHostName())
1042 .append(" at ")
1043 .append(outgoingMailServer.getHost())
1044 .append(" for ")
1045 .append(mail.getRecipients());
1046 log(logMessageBuffer.toString());
1047 return true;
1048 } catch (SendFailedException sfe) {
1049 logSendFailedException(sfe);
1050
1051 if (sfe.getValidSentAddresses() != null) {
1052 Address[] validSent = sfe.getValidSentAddresses();
1053 if (validSent.length > 0) {
1054 StringBuffer logMessageBuffer =
1055 new StringBuffer(256)
1056 .append("Mail (")
1057 .append(mail.getName())
1058 .append(") sent successfully for ")
1059 .append(Arrays.asList(validSent));
1060 log(logMessageBuffer.toString());
1061 }
1062 }
1063
1064 /* SMTPSendFailedException introduced in JavaMail 1.3.2, and provides detailed protocol reply code for the operation */
1065 if (sfe.getClass().getName().endsWith(".SMTPSendFailedException")) {
1066 try {
1067 int returnCode = ((Integer) invokeGetter(sfe, "getReturnCode")).intValue();
1068 // if 5xx, terminate this delivery attempt by re-throwing the exception.
1069 if (returnCode >= 500 && returnCode <= 599) throw sfe;
1070 } catch (ClassCastException cce) {
1071 } catch (IllegalArgumentException iae) {
1072 }
1073 }
1074
1075 if (sfe.getValidUnsentAddresses() != null
1076 && sfe.getValidUnsentAddresses().length > 0) {
1077 if (isDebug) log("Send failed, " + sfe.getValidUnsentAddresses().length + " valid addresses remain, continuing with any other servers");
1078 lastError = sfe;
1079 continue;
1080 } else {
1081 // There are no valid addresses left to send, so rethrow
1082 throw sfe;
1083 }
1084 } catch (MessagingException me) {
1085 //MessagingException are horribly difficult to figure out what actually happened.
1086 StringBuffer exceptionBuffer =
1087 new StringBuffer(256)
1088 .append("Exception delivering message (")
1089 .append(mail.getName())
1090 .append(") - ")
1091 .append(me.getMessage());
1092 log(exceptionBuffer.toString());
1093 if ((me.getNextException() != null) &&
1094 (me.getNextException() instanceof java.io.IOException)) {
1095 //This is more than likely a temporary failure
1096
1097 // If it's an IO exception with no nested exception, it's probably
1098 // some socket or weird I/O related problem.
1099 lastError = me;
1100 continue;
1101 }
1102 // This was not a connection or I/O error particular to one
1103 // SMTP server of an MX set. Instead, it is almost certainly
1104 // a protocol level error. In this case we assume that this
1105 // is an error we'd encounter with any of the SMTP servers
1106 // associated with this MX record, and we pass the exception
1107 // to the code in the outer block that determines its severity.
1108 throw me;
1109 }
1110 } // end while
1111 //If we encountered an exception while looping through,
1112 //throw the last MessagingException we caught. We only
1113 //do this if we were unable to send the message to any
1114 //server. If sending eventually succeeded, we exit
1115 //deliver() though the return at the end of the try
1116 //block.
1117 if (lastError != null) {
1118 throw lastError;
1119 }
1120 } catch (SendFailedException sfe) {
1121 logSendFailedException(sfe);
1122
1123 Collection recipients = mail.getRecipients();
1124
1125 boolean deleteMessage = false;
1126
1127 /*
1128 * If you send a message that has multiple invalid
1129 * addresses, you'll get a top-level SendFailedException
1130 * that that has the valid, valid-unsent, and invalid
1131 * address lists, with all of the server response messages
1132 * will be contained within the nested exceptions. [Note:
1133 * the content of the nested exceptions is implementation
1134 * dependent.]
1135 *
1136 * sfe.getInvalidAddresses() should be considered permanent.
1137 * sfe.getValidUnsentAddresses() should be considered temporary.
1138 *
1139 * JavaMail v1.3 properly populates those collections based
1140 * upon the 4xx and 5xx response codes to RCPT TO. Some
1141 * servers, such as Yahoo! don't respond to the RCPT TO,
1142 * and provide a 5xx reply after DATA. In that case, we
1143 * will pick up the failure from SMTPSendFailedException.
1144 *
1145 */
1146
1147 /* SMTPSendFailedException introduced in JavaMail 1.3.2, and provides detailed protocol reply code for the operation */
1148 try {
1149 if (sfe.getClass().getName().endsWith(".SMTPSendFailedException")) {
1150 int returnCode = ((Integer) invokeGetter(sfe, "getReturnCode")).intValue();
1151 // If we got an SMTPSendFailedException, use its RetCode to determine default permanent/temporary failure
1152 deleteMessage = (returnCode >= 500 && returnCode <= 599);
1153 } else {
1154 // Sometimes we'll get a normal SendFailedException with nested SMTPAddressFailedException, so use the latter RetCode
1155 MessagingException me = sfe;
1156 Exception ne;
1157 while ((ne = me.getNextException()) != null && ne instanceof MessagingException) {
1158 me = (MessagingException)ne;
1159 if (me.getClass().getName().endsWith(".SMTPAddressFailedException")) {
1160 int returnCode = ((Integer) invokeGetter(me, "getReturnCode")).intValue();
1161 deleteMessage = (returnCode >= 500 && returnCode <= 599);
1162 }
1163 }
1164 }
1165 } catch (IllegalStateException ise) {
1166 // unexpected exception (not a compatible javamail implementation)
1167 } catch (ClassCastException cce) {
1168 // unexpected exception (not a compatible javamail implementation)
1169 }
1170
1171 // log the original set of intended recipients
1172 if (isDebug) log("Recipients: " + recipients);
1173
1174 if (sfe.getInvalidAddresses() != null) {
1175 Address[] address = sfe.getInvalidAddresses();
1176 if (address.length > 0) {
1177 recipients.clear();
1178 for (int i = 0; i < address.length; i++) {
1179 try {
1180 recipients.add(new MailAddress(address[i].toString()));
1181 } catch (ParseException pe) {
1182 // this should never happen ... we should have
1183 // caught malformed addresses long before we
1184 // got to this code.
1185 log("Can't parse invalid address: " + pe.getMessage());
1186 }
1187 }
1188 if (isDebug) log("Invalid recipients: " + recipients);
1189 deleteMessage = failMessage(mail, sfe, true);
1190 }
1191 }
1192
1193 if (sfe.getValidUnsentAddresses() != null) {
1194 Address[] address = sfe.getValidUnsentAddresses();
1195 if (address.length > 0) {
1196 recipients.clear();
1197 for (int i = 0; i < address.length; i++) {
1198 try {
1199 recipients.add(new MailAddress(address[i].toString()));
1200 } catch (ParseException pe) {
1201 // this should never happen ... we should have
1202 // caught malformed addresses long before we
1203 // got to this code.
1204 log("Can't parse unsent address: " + pe.getMessage());
1205 }
1206 }
1207 if (isDebug) log("Unsent recipients: " + recipients);
1208 if (sfe.getClass().getName().endsWith(".SMTPSendFailedException")) {
1209 int returnCode = ((Integer) invokeGetter(sfe, "getReturnCode")).intValue();
1210 deleteMessage = failMessage(mail, sfe, returnCode >= 500 && returnCode <= 599);
1211 } else {
1212 deleteMessage = failMessage(mail, sfe, false);
1213 }
1214 }
1215 }
1216
1217 return deleteMessage;
1218 } catch (MessagingException ex) {
1219 // We should do a better job checking this... if the failure is a general
1220 // connect exception, this is less descriptive than more specific SMTP command
1221 // failure... have to lookup and see what are the various Exception
1222 // possibilities
1223
1224 // Unable to deliver message after numerous tries... fail accordingly
1225
1226 // We check whether this is a 5xx error message, which
1227 // indicates a permanent failure (like account doesn't exist
1228 // or mailbox is full or domain is setup wrong).
1229 // We fail permanently if this was a 5xx error
1230 return failMessage(mail, ex, ('5' == ex.getMessage().charAt(0)));
1231 } catch (Exception ex) {
1232 // Generic exception = permanent failure
1233 return failMessage(mail, ex, true);
1234 }
1235
1236 /* If we get here, we've exhausted the loop of servers without
1237 * sending the message or throwing an exception. One case
1238 * where this might happen is if we get a MessagingException on
1239 * each transport.connect(), e.g., if there is only one server
1240 * and we get a connect exception.
1241 */
1242 return failMessage(mail, new MessagingException("No mail server(s) available at this time."), false);
1243 }
1244
1245
1246
1247
1248
1249
1250 /**
1251 * Try to return a usefull logString created of the Exception which was given.
1252 * Return null if nothing usefull could be done
1253 *
1254 * @param e The MessagingException to use
1255 * @return logString
1256 */
1257 private String exceptionToLogString(Exception e) {
1258 if (e.getClass().getName().endsWith(".SMTPSendFailedException")) {
1259 return "RemoteHost said: " + e.getMessage();
1260 } else if (e instanceof SendFailedException) {
1261 SendFailedException exception = (SendFailedException) e;
1262
1263 // No error
1264 if ( exception.getInvalidAddresses().length == 0 &&
1265 exception.getValidUnsentAddresses().length == 0) return null;
1266
1267 Exception ex;
1268 StringBuffer sb = new StringBuffer();
1269 boolean smtpExFound = false;
1270 sb.append("RemoteHost said:");
1271
1272
1273 if (e instanceof MessagingException) while((ex = ((MessagingException) e).getNextException()) != null && ex instanceof MessagingException) {
1274 e = ex;
1275 if (ex.getClass().getName().endsWith(".SMTPAddressFailedException")) {
1276 try {
1277 InternetAddress ia = (InternetAddress) invokeGetter(ex, "getAddress");
1278 sb.append(" ( " + ia + " - [" + ex.getMessage().replaceAll("\\n", "") + "] )");
1279 smtpExFound = true;
1280 } catch (IllegalStateException ise) {
1281 // Error invoking the getAddress method
1282 } catch (ClassCastException cce) {
1283 // The getAddress method returned something different than InternetAddress
1284 }
1285 }
1286 }
1287 if (!smtpExFound) {
1288 boolean invalidAddr = false;
1289 sb.append(" ( ");
1290
1291 if (exception.getInvalidAddresses().length > 0) {
1292 sb.append(exception.getInvalidAddresses());
1293 invalidAddr = true;
1294 }
1295 if (exception.getValidUnsentAddresses().length > 0) {
1296 if (invalidAddr == true) sb.append(" " );
1297 sb.append(exception.getValidUnsentAddresses());
1298 }
1299 sb.append(" - [");
1300 sb.append(exception.getMessage().replaceAll("\\n", ""));
1301 sb.append("] )");
1302 }
1303 return sb.toString();
1304 }
1305 return null;
1306 }
1307
1308 /**
1309 * Utility method used to invoke getters for javamail implementation specific classes.
1310 *
1311 * @param target the object whom method will be invoked
1312 * @param getter the no argument method name
1313 * @return the result object
1314 * @throws IllegalStateException on invocation error
1315 */
1316 private Object invokeGetter(Object target, String getter) {
1317 try {
1318 Method getAddress = target.getClass().getMethod(getter, null);
1319 return getAddress.invoke(target, null);
1320 } catch (NoSuchMethodException nsme) {
1321 // An SMTPAddressFailedException with no getAddress method.
1322 } catch (IllegalAccessException iae) {
1323 } catch (IllegalArgumentException iae) {
1324 } catch (InvocationTargetException ite) {
1325 // Other issues with getAddress invokation.
1326 }
1327 return new IllegalStateException("Exception invoking "+getter+" on a "+target.getClass()+" object");
1328 }
1329
1330 /*
1331 * private method to log the extended SendFailedException introduced in JavaMail 1.3.2.
1332 */
1333 private void logSendFailedException(SendFailedException sfe) {
1334 if (isDebug) {
1335 MessagingException me = sfe;
1336 if (me.getClass().getName().endsWith(".SMTPSendFailedException")) {
1337 try {
1338 String command = (String) invokeGetter(sfe, "getCommand");
1339 Integer returnCode = (Integer) invokeGetter(sfe, "getReturnCode");
1340 log("SMTP SEND FAILED:");
1341 log(sfe.toString());
1342 log(" Command: " + command);
1343 log(" RetCode: " + returnCode);
1344 log(" Response: " + sfe.getMessage());
1345 } catch (IllegalStateException ise) {
1346 // Error invoking the getAddress method
1347 log("Send failed: " + me.toString());
1348 } catch (ClassCastException cce) {
1349 // The getAddress method returned something different than InternetAddress
1350 log("Send failed: " + me.toString());
1351 }
1352 } else {
1353 log("Send failed: " + me.toString());
1354 }
1355 Exception ne;
1356 while ((ne = me.getNextException()) != null && ne instanceof MessagingException) {
1357 me = (MessagingException)ne;
1358 if (me.getClass().getName().endsWith(".SMTPAddressFailedException") || me.getClass().getName().endsWith(".SMTPAddressSucceededException")) {
1359 try {
1360 String action = me.getClass().getName().endsWith(".SMTPAddressFailedException") ? "FAILED" : "SUCCEEDED";
1361 InternetAddress address = (InternetAddress) invokeGetter(me, "getAddress");
1362 String command = (String) invokeGetter(me, "getCommand");
1363 Integer returnCode = (Integer) invokeGetter(me, "getReturnCode");
1364 log("ADDRESS "+action+":");
1365 log(me.toString());
1366 log(" Address: " + address);
1367 log(" Command: " + command);
1368 log(" RetCode: " + returnCode);
1369 log(" Response: " + me.getMessage());
1370 } catch (IllegalStateException ise) {
1371 // Error invoking the getAddress method
1372 } catch (ClassCastException cce) {
1373 // A method returned something different than expected
1374 }
1375 }
1376 }
1377 }
1378 }
1379
1380 /**
1381 * Converts a message to 7 bit.
1382 *
1383 * @param part
1384 */
1385 private void convertTo7Bit(MimePart part) throws MessagingException, IOException {
1386 if (part.isMimeType("multipart/*")) {
1387 MimeMultipart parts = (MimeMultipart) part.getContent();
1388 int count = parts.getCount();
1389 for (int i = 0; i < count; i++) {
1390 convertTo7Bit((MimePart)parts.getBodyPart(i));
1391 }
1392 } else if ("8bit".equals(part.getEncoding())) {
1393 // The content may already be in encoded the form (likely with mail created from a
1394 // stream). In that case, just changing the encoding to quoted-printable will mangle
1395 // the result when this is transmitted. We must first convert the content into its
1396 // native format, set it back, and only THEN set the transfer encoding to force the
1397 // content to be encoded appropriately.
1398
1399 // if the part doesn't contain text it will be base64 encoded.
1400 String contentTransferEncoding = part.isMimeType("text/*") ? "quoted-printable" : "base64";
1401 part.setContent(part.getContent(), part.getContentType());
1402 part.setHeader("Content-Transfer-Encoding", contentTransferEncoding);
1403 part.addHeader("X-MIME-Autoconverted", "from 8bit to "+contentTransferEncoding+" by "+getMailetContext().getServerInfo());
1404 }
1405 }
1406
1407 /**
1408 * Insert the method's description here.
1409 * Creation date: (2/25/00 1:14:18 AM)
1410 * @param mail org.apache.james.core.MailImpl
1411 * @param ex javax.mail.MessagingException
1412 * @param permanent
1413 * @return boolean Whether the message failed fully and can be deleted
1414 */
1415 private boolean failMessage(Mail mail, Exception ex, boolean permanent) {
1416 StringWriter sout = new StringWriter();
1417 PrintWriter out = new PrintWriter(sout, true);
1418 if (permanent) {
1419 out.print("Permanent");
1420 } else {
1421 out.print("Temporary");
1422 }
1423
1424 String exceptionLog = exceptionToLogString(ex);
1425
1426 StringBuffer logBuffer =
1427 new StringBuffer(64)
1428 .append(" exception delivering mail (")
1429 .append(mail.getName());
1430
1431 if (exceptionLog != null) {
1432 logBuffer.append(". ");
1433 logBuffer.append(exceptionLog);
1434 }
1435
1436 logBuffer.append(": ");
1437 out.print(logBuffer.toString());
1438 if (isDebug) ex.printStackTrace(out);
1439 log(sout.toString());
1440 if (!permanent) {
1441 if (!mail.getState().equals(Mail.ERROR)) {
1442 mail.setState(Mail.ERROR);
1443 mail.setErrorMessage("0");
1444 mail.setLastUpdated(new Date());
1445 }
1446
1447 int retries = 0;
1448 try {
1449 retries = Integer.parseInt(mail.getErrorMessage());
1450 } catch (NumberFormatException e) {
1451 // Something strange was happen with the errorMessage..
1452 }
1453
1454 if (retries < maxRetries) {
1455 logBuffer =
1456 new StringBuffer(128)
1457 .append("Storing message ")
1458 .append(mail.getName())
1459 .append(" into outgoing after ")
1460 .append(retries)
1461 .append(" retries");
1462 log(logBuffer.toString());
1463 ++retries;
1464 mail.setErrorMessage(retries + "");
1465 mail.setLastUpdated(new Date());
1466 return false;
1467 } else {
1468 logBuffer =
1469 new StringBuffer(128)
1470 .append("Bouncing message ")
1471 .append(mail.getName())
1472 .append(" after ")
1473 .append(retries)
1474 .append(" retries");
1475 log(logBuffer.toString());
1476 }
1477 }
1478
1479 if (mail.getSender() == null) {
1480 log("Null Sender: no bounce will be generated for " + mail.getName());
1481 return true;
1482 }
1483
1484 if (bounceProcessor != null) {
1485 // do the new DSN bounce
1486 // setting attributes for DSN mailet
1487 mail.setAttribute("delivery-error", ex);
1488 mail.setState(bounceProcessor);
1489 // re-insert the mail into the spool for getting it passed to the dsn-processor
1490 MailetContext mc = getMailetContext();
1491 try {
1492 mc.sendMail(mail);
1493 } catch (MessagingException e) {
1494 // we shouldn't get an exception, because the mail was already processed
1495 log("Exception re-inserting failed mail: ", e);
1496 }
1497 } else {
1498 // do an old style bounce
1499 bounce(mail, ex);
1500 }
1501 return true;
1502 }
1503
1504 private void bounce(Mail mail, Exception ex) {
1505 StringWriter sout = new StringWriter();
1506 PrintWriter out = new PrintWriter(sout, true);
1507 String machine = "[unknown]";
1508 try {
1509 machine = getMailetContext().getAttribute(Constants.HOSTNAME).toString();
1510
1511 } catch(Exception e){
1512 machine = "[address unknown]";
1513 }
1514 StringBuffer bounceBuffer =
1515 new StringBuffer(128)
1516 .append("Hi. This is the James mail server at ")
1517 .append(machine)
1518 .append(".");
1519 out.println(bounceBuffer.toString());
1520 out.println("I'm afraid I wasn't able to deliver your message to the following addresses.");
1521 out.println("This is a permanent error; I've given up. Sorry it didn't work out. Below");
1522 out.println("I include the list of recipients and the reason why I was unable to deliver");
1523 out.println("your message.");
1524 out.println();
1525 for (Iterator i = mail.getRecipients().iterator(); i.hasNext(); ) {
1526 out.println(i.next());
1527 }
1528 if (ex instanceof MessagingException) {
1529 if (((MessagingException) ex).getNextException() == null) {
1530 out.println(ex.getMessage().trim());
1531 } else {
1532 Exception ex1 = ((MessagingException) ex).getNextException();
1533 if (ex1 instanceof SendFailedException) {
1534 out.println("Remote mail server told me: " + ex1.getMessage().trim());
1535 } else if (ex1 instanceof UnknownHostException) {
1536 out.println("Unknown host: " + ex1.getMessage().trim());
1537 out.println("This could be a DNS server error, a typo, or a problem with the recipient's mail server.");
1538 } else if (ex1 instanceof ConnectException) {
1539 //Already formatted as "Connection timed out: connect"
1540 out.println(ex1.getMessage().trim());
1541 } else if (ex1 instanceof SocketException) {
1542 out.println("Socket exception: " + ex1.getMessage().trim());
1543 } else {
1544 out.println(ex1.getMessage().trim());
1545 }
1546 }
1547 }
1548 out.println();
1549
1550 log("Sending failure message " + mail.getName());
1551 try {
1552 getMailetContext().bounce(mail, sout.toString());
1553 } catch (MessagingException me) {
1554 log("Encountered unexpected messaging exception while bouncing message: " + me.getMessage());
1555 } catch (Exception e) {
1556 log("Encountered unexpected exception while bouncing message: " + e.getMessage());
1557 }
1558 }
1559
1560 /**
1561 * Returns the javamail Session object.
1562 * @param props
1563 * @param authenticator
1564 * @return
1565 */
1566 protected Session obtainSession(Properties props) {
1567 return Session.getInstance(props);
1568 }
1569
1570 /**
1571 * Returns an Iterator over org.apache.mailet.HostAddress, a
1572 * specialized subclass of javax.mail.URLName, which provides
1573 * location information for servers that are specified as mail
1574 * handlers for the given hostname. If no host is found, the
1575 * Iterator returned will be empty and the first call to hasNext()
1576 * will return false. The Iterator is a nested iterator: the outer
1577 * iteration is over each gateway, and the inner iteration is over
1578 * potentially multiple A records for each gateway.
1579 *
1580 * @see org.apache.james.DNSServer#getSMTPHostAddresses(String)
1581 * @since v2.2.0a16-unstable
1582 * @param gatewayServers - Collection of host[:port] Strings
1583 * @return an Iterator over HostAddress instances, sorted by priority
1584 */
1585 private Iterator getGatewaySMTPHostAddresses(final Collection gatewayServers) {
1586 return new Iterator() {
1587 private Iterator gateways = gatewayServers.iterator();
1588 private Iterator addresses = null;
1589
1590 public boolean hasNext() {
1591 /* Make sure that when next() is called, that we can
1592 * provide a HostAddress. This means that we need to
1593 * have an inner iterator, and verify that it has
1594 * addresses. We could, for example, run into a
1595 * situation where the next gateway didn't have any
1596 * valid addresses.
1597 */
1598 if (!hasNextAddress() && gateways.hasNext()) {
1599 do {
1600 String server = (String) gateways.next();
1601 String port = "25";
1602
1603 int idx = server.indexOf(':');
1604 if ( idx > 0) {
1605 port = server.substring(idx+1);
1606 server = server.substring(0,idx);
1607 }
1608
1609 final String nextGateway = server;
1610 final String nextGatewayPort = port;
1611 try {
1612 final InetAddress[] ips = dnsServer.getAllByName(nextGateway);
1613 addresses = new Iterator() {
1614 private InetAddress[] ipAddresses = ips;
1615 int i = 0;
1616
1617 public boolean hasNext() {
1618 return i < ipAddresses.length;
1619 }
1620
1621 public Object next() {
1622 return new org.apache.mailet.HostAddress(nextGateway, "smtp://" + (ipAddresses[i++]).getHostAddress() + ":" + nextGatewayPort);
1623 }
1624
1625 public void remove() {
1626 throw new UnsupportedOperationException ("remove not supported by this iterator");
1627 }
1628 };
1629 }
1630 catch (java.net.UnknownHostException uhe) {
1631 log("Unknown gateway host: " + uhe.getMessage().trim());
1632 log("This could be a DNS server error or configuration error.");
1633 }
1634 } while (!hasNextAddress() && gateways.hasNext());
1635 }
1636
1637 return hasNextAddress();
1638 }
1639
1640 private boolean hasNextAddress() {
1641 return addresses != null && addresses.hasNext();
1642 }
1643
1644 public Object next() {
1645 return (addresses != null) ? addresses.next() : null;
1646 }
1647
1648 public void remove() {
1649 throw new UnsupportedOperationException ("remove not supported by this iterator");
1650 }
1651 };
1652 }
1653
1654 /**
1655 * Setter for the dnsserver service
1656 * @param dnsServer dns service
1657 */
1658 protected synchronized void setDNSServer(DNSService dnsServer) {
1659 this.dnsServer = dnsServer;
1660 }
1661 }