View Javadoc

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 }