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  package org.apache.james.transport.mailets;
21  
22  import org.apache.avalon.cornerstone.services.store.Store;
23  import org.apache.avalon.framework.configuration.DefaultConfiguration;
24  import org.apache.avalon.framework.container.ContainerUtil;
25  import org.apache.avalon.framework.service.ServiceException;
26  import org.apache.avalon.framework.service.ServiceManager;
27  import org.apache.james.Constants;
28  import org.apache.james.services.SpoolRepository;
29  import org.apache.mailet.base.GenericMailet;
30  import org.apache.mailet.Mail;
31  import org.apache.mailet.MailetContext;
32  import org.apache.oro.text.regex.MalformedPatternException;
33  import org.apache.oro.text.regex.MatchResult;
34  import org.apache.oro.text.regex.Pattern;
35  import org.apache.oro.text.regex.Perl5Compiler;
36  import org.apache.oro.text.regex.Perl5Matcher;
37  
38  import javax.mail.MessagingException;
39  
40  import java.util.ArrayList;
41  import java.util.Collection;
42  import java.util.Date;
43  import java.util.HashMap;
44  import java.util.Iterator;
45  import java.util.Locale;
46  import java.util.StringTokenizer;
47  import java.util.Vector;
48  
49  /**
50   * This Mailet retries delivery of a mail based on schedule specified in the
51   * James configuration file by the 'delayTime' attribute. The format of the
52   * 'delayTime' attribute is: [attempts*]delay[units]
53   * <p>
54   * For example, if the delay times were specified as follows:<br>
55   * <delayTime> 4*15 minutes </delayTime> <delayTime> 3*1 hour </delayTime>
56   * <delayTime> 3*4 hours </delayTime>
57   * 
58   * <maxRetries> 10 </maxRetries>
59   * 
60   * after the initial failure, the message will be retried by sending it to the
61   * processor specified by the 'retryProcessor' attribute, as per the following
62   * schedule: 1) 4 attempts will be made every 15 minutes. 2) 3 attempts will be
63   * made every hour. 3) 3 attempts will be made every 4 hours.
64   * 
65   * If the message still fails, it will be sent for error processing to the
66   * processor specified by the 'errorProcessor' attribute.
67   * 
68   * <p>
69   * Following list summarizes all the attributes of this Mailet that can be
70   * configured:
71   * <ul>
72   * <li><b>retryRepository</b> - Spool repository where mails are stored.
73   * <li><b>delayTime</b> - Delay time (See description above).
74   * <li><b>maxRetries</b> - Maximum no. of retry attempts.
75   * <li><b>retryThreads</b> - No. of Threads used for retrying.
76   * <li><b>retryProcessor</b> - Processor used for retrying.
77   * <li><b>errorProcessor</b> - Error processor that will be used when all retry
78   * attempts fail.
79   * <li><b>isDebug</b> - Can be set to 'true' for debugging.
80   * </ul>
81   * 
82   */
83  public class Retry extends GenericMailet implements Runnable {
84      // Mail attribute that keeps track of # of retries.
85      private static final String RETRY_COUNT = "RETRY_COUNT";
86  
87      // Mail attribute that keeps track of original error message.
88      public static final String ORIGINAL_ERROR = "originalError";
89  
90      // Default Delay Time (Default is 6*60*60*1000 Milliseconds (6 hours)).
91      private static final long DEFAULT_DELAY_TIME = 21600000;
92  
93      // Pattern to match [attempts*]delay[units].
94      private static final String PATTERN_STRING = "\\s*([0-9]*\\s*[\\*])?\\s*([0-9]+)\\s*([a-z,A-Z]*)\\s*";
95  
96      // Compiled pattern of the above String.
97      private static Pattern PATTERN = null;
98  
99      // Holds allowed units for delayTime together with factor to turn it into
100     // the
101     // equivalent time in milliseconds.
102     private static final HashMap MULTIPLIERS = new HashMap(10);
103 
104     /*
105      * Compiles pattern for processing delayTime entries. <p>Initializes
106      * MULTIPLIERS with the supported unit quantifiers.
107      */
108     static {
109         try {
110             Perl5Compiler compiler = new Perl5Compiler();
111             PATTERN = compiler.compile(PATTERN_STRING,
112                     Perl5Compiler.READ_ONLY_MASK);
113         } catch (MalformedPatternException mpe) {
114             // This should never happen as the pattern string is hard coded.
115             System.err.println("Malformed pattern: " + PATTERN_STRING);
116             mpe.printStackTrace(System.err);
117         }
118 
119         // Add allowed units and their respective multiplier.
120         MULTIPLIERS.put("msec", new Integer(1));
121         MULTIPLIERS.put("msecs", new Integer(1));
122         MULTIPLIERS.put("sec", new Integer(1000));
123         MULTIPLIERS.put("secs", new Integer(1000));
124         MULTIPLIERS.put("minute", new Integer(1000 * 60));
125         MULTIPLIERS.put("minutes", new Integer(1000 * 60));
126         MULTIPLIERS.put("hour", new Integer(1000 * 60 * 60));
127         MULTIPLIERS.put("hours", new Integer(1000 * 60 * 60));
128         MULTIPLIERS.put("day", new Integer(1000 * 60 * 60 * 24));
129         MULTIPLIERS.put("days", new Integer(1000 * 60 * 60 * 24));
130     }
131 
132     /**
133      * Used in the accept call to the spool. It will select the next mail ready
134      * for processing according to the mails 'retrycount' and 'lastUpdated'
135      * time.
136      **/
137     private class MultipleDelayFilter implements SpoolRepository.AcceptFilter {
138         /**
139          * Holds the time to wait for the youngest mail to get ready for
140          * processing.
141          **/
142         long youngest = 0;
143 
144         /**
145          * Uses the getNextDelay to determine if a mail is ready for processing
146          * based on the delivered parameters errorMessage (which holds the
147          * retrycount), lastUpdated and state.
148          * 
149          * @param key
150          *            the name/key of the message
151          * @param state
152          *            the mails state
153          * @param lastUpdated
154          *            the mail was last written to the spool at this time.
155          * @param errorMessage
156          *            actually holds the retrycount as a string
157          * @return {@code true} if message is ready for processing else {@code
158          *         false}
159          **/
160         public boolean accept(String key, String state, long lastUpdated,
161                 String errorMessage) {
162             int retries = Integer.parseInt(errorMessage);
163 
164             long delay = getNextDelay(retries);
165             long timeToProcess = delay + lastUpdated;
166 
167             if (System.currentTimeMillis() > timeToProcess) {
168                 // We're ready to process this again
169                 return true;
170             } else {
171                 // We're not ready to process this.
172                 if (youngest == 0 || youngest > timeToProcess) {
173                     // Mark this as the next most likely possible mail to
174                     // process
175                     youngest = timeToProcess;
176                 }
177                 return false;
178             }
179         }
180 
181         /**
182          * Returns the optimal time the SpoolRepository.accept(AcceptFilter)
183          * method should wait before trying to find a mail ready for processing
184          * again.
185          **/
186         public long getWaitTime() {
187             if (youngest == 0) {
188                 return 0;
189             } else {
190                 long duration = youngest - System.currentTimeMillis();
191                 youngest = 0;
192                 return duration <= 0 ? 1 : duration;
193             }
194         }
195     }
196     
197     // Flag to define verbose logging messages.
198     private boolean isDebug = false;
199 
200     // Repository used to store messages that will be retried.
201     private SpoolRepository workRepository;
202 
203     // List of Delay Times. Controls frequency of retry attempts.
204     private long[] delayTimes;
205 
206     // Maximum no. of retries (Defaults to 5).
207     private int maxRetries = 5;
208 
209     // No. of threads used to process messages that should be retried.
210     private int workersThreadCount = 1;
211 
212     // Collection that stores all worker threads.
213     private Collection workersThreads = new Vector();
214 
215     // Processor that will be called for retrying. Defaults to "root" processor.
216     private String retryProcessor = Mail.DEFAULT;
217 
218     // Processor that will be called if retrying fails after trying maximum no.
219     // of
220     // times. Defaults to "error" processor.
221     private String errorProcessor = Mail.ERROR;
222 
223     // Flag used by 'run' method to end itself.
224     private volatile boolean destroyed = false;
225 
226     // Matcher used in 'init' method to parse delayTimes specified in config
227     // file.
228     private Perl5Matcher delayTimeMatcher;
229 
230     // Filter used by 'accept' to check if message is ready for retrying.
231     private MultipleDelayFilter delayFilter = new MultipleDelayFilter();
232 
233     // Path of the retry repository
234     private String workRepositoryPath = null;
235 
236     /**
237      * Initializes all arguments based on configuration values specified in the
238      * James configuration file.
239      * 
240      * @throws MessagingException
241      *             on failure to initialize attributes.
242      */
243     public void init() throws MessagingException {
244         // Set isDebug flag.
245         isDebug = (getInitParameter("debug") == null) ? false : new Boolean(getInitParameter("debug")).booleanValue();
246 
247         // Create list of Delay Times.
248         ArrayList delayTimesList = new ArrayList();
249         try {
250             if (getInitParameter("delayTime") != null) {
251                 delayTimeMatcher = new Perl5Matcher();
252                 String delayTimesParm = getInitParameter("delayTime");
253 
254                 // Split on commas
255                 StringTokenizer st = new StringTokenizer (delayTimesParm,",");
256                 while (st.hasMoreTokens()) {
257                     String delayTime = st.nextToken();
258                     delayTimesList.add (new Delay(delayTime));
259                 }
260             } else {
261                 // Use default delayTime.
262                 delayTimesList.add(new Delay());
263             }
264         } catch (Exception e) {
265             log("Invalid delayTime setting: " + getInitParameter("delayTime"));
266         }
267 
268         try {
269             // Get No. of Max Retries.
270             if (getInitParameter("maxRetries") != null) {
271                 maxRetries = Integer.parseInt(getInitParameter("maxRetries"));
272             }
273     
274             // Check consistency of 'maxRetries' with delayTimesList attempts.
275             int totalAttempts = calcTotalAttempts(delayTimesList);
276     
277             // If inconsistency found, fix it.
278             if (totalAttempts > maxRetries) {
279                 log("Total number of delayTime attempts exceeds maxRetries specified. "
280                         + " Increasing maxRetries from "
281                         + maxRetries
282                         + " to "
283                         + totalAttempts);
284                 maxRetries = totalAttempts;
285             } else {
286                 int extra = maxRetries - totalAttempts;
287                 if (extra != 0) {
288                     log("maxRetries is larger than total number of attempts specified.  "
289                             + "Increasing last delayTime with "
290                             + extra
291                             + " attempts ");
292     
293                     // Add extra attempts to the last delayTime.
294                     if (delayTimesList.size() != 0) {
295                         // Get the last delayTime.
296                         Delay delay = (Delay) delayTimesList.get(delayTimesList
297                                 .size() - 1);
298     
299                         // Increase no. of attempts.
300                         delay.setAttempts(delay.getAttempts() + extra);
301                         log("Delay of " + delay.getDelayTime()
302                                 + " msecs is now attempted: " + delay.getAttempts()
303                                 + " times");
304                     } else {
305                         throw new MessagingException(
306                                 "No delaytimes, cannot continue");
307                     }
308                 }
309             }
310             delayTimes = expandDelays(delayTimesList);
311         } catch (Exception e) {
312             log("Invalid maxRetries setting: " + getInitParameter("maxRetries"));
313         }
314 
315         ServiceManager compMgr = (ServiceManager) getMailetContext()
316                 .getAttribute(Constants.AVALON_COMPONENT_MANAGER);
317 
318         // Get the path for the 'Retry' repository. This is the place on the
319         // file system where Mail objects will be saved during the 'retry'
320         // processing. This can be changed to a repository on a database (e.g.
321         // db://maildb/spool/retry).
322         workRepositoryPath = getInitParameter("retryRepository");
323         if (workRepositoryPath == null) {
324             workRepositoryPath = "file://var/mail/retry/";
325         }
326 
327         try {
328             // Instantiate a MailRepository for mails that should be retried.
329             Store mailstore = (Store) compMgr.lookup(Store.ROLE);
330 
331             DefaultConfiguration spoolConf = new DefaultConfiguration(
332                     "repository", "generated:Retry");
333             spoolConf.setAttribute("destinationURL", workRepositoryPath);
334             spoolConf.setAttribute("type", "SPOOL");
335             workRepository = (SpoolRepository) mailstore.select(spoolConf);
336         } catch (ServiceException cnfe) {
337             log("Failed to retrieve Store component:" + cnfe.getMessage());
338             throw new MessagingException("Failed to retrieve Store component",
339                     cnfe);
340         }
341 
342         // Start Workers Threads.
343         workersThreadCount = Integer.parseInt(getInitParameter("retryThreads"));
344         for (int i = 0; i < workersThreadCount; i++) {
345             String threadName = "Retry thread (" + i + ")";
346             Thread t = new Thread(this, threadName);
347             t.start();
348             workersThreads.add(t);
349         }
350 
351         // Get Retry Processor
352         String processor = getInitParameter("retryProcessor");
353         retryProcessor = (processor == null) ? Mail.DEFAULT : processor;
354 
355         // Get Error Processor
356         processor = getInitParameter("errorProcessor");
357         errorProcessor = (processor == null) ? Mail.ERROR : processor;
358     }
359 
360     /**
361      * Calculates Total no. of attempts for the specified delayList.
362      * 
363      * @param delayList
364      *            list of 'Delay' objects
365      * @return total no. of retry attempts
366      */
367     private int calcTotalAttempts (ArrayList delayList) {
368         int sum = 0;
369         Iterator i = delayList.iterator();
370         while (i.hasNext()) {
371             Delay delay = (Delay)i.next();
372             sum += delay.getAttempts();
373         }
374         return sum;
375     }
376 
377     /**
378      * Expands an ArrayList containing Delay objects into an array holding the
379      * only delaytime in the order.
380      * <p>
381      * 
382      * For example, if the list has 2 Delay objects : First having attempts=2
383      * and delaytime 4000 Second having attempts=1 and delaytime=300000
384      * 
385      * This will be expanded into this array:
386      * <p>
387      * 
388      * long[0] = 4000
389      * <p>
390      * long[1] = 4000
391      * <p>
392      * long[2] = 300000
393      * <p>
394      * 
395      * @param delayList
396      *            the list to expand
397      * @return the expanded list
398      **/
399     private long[] expandDelays(ArrayList delayList) {
400         long[] delays = new long[calcTotalAttempts(delayList)];
401         int idx = 0;
402         for (int i = 0; i < delayList.size(); i++) {
403             for (int j = 0; j < ((Delay) delayList.get(i)).getAttempts(); j++) {
404                 delays[idx++] = ((Delay) delayList.get(i)).getDelayTime();
405             }
406         }
407         return delays;
408     }
409 
410     /**
411      * Returns, given a retry count, the next delay time to use.
412      * 
413      * @param retryCount
414      *            the current retry count.
415      * @return the next delay time to use
416      **/
417     private long getNextDelay(int retryCount) {
418         if (retryCount > delayTimes.length) {
419             return DEFAULT_DELAY_TIME;
420         }
421         return delayTimes[retryCount];
422     }
423 
424 
425     /**
426      * This class is used to hold a delay time and its corresponding number of
427      * retries.
428      **/
429     private class Delay {
430         private int attempts = 1;
431 
432         private long delayTime = DEFAULT_DELAY_TIME;
433 
434         /**
435          * This constructor expects Strings of the form
436          * "[attempt\*]delaytime[unit]".
437          * <p>
438          * The optional attempt is the number of tries this delay should be used
439          * (default = 1). The unit, if present, must be one of
440          * (msec,sec,minute,hour,day). The default value of unit is 'msec'.
441          * <p>
442          * The constructor multiplies the delaytime by the relevant multiplier
443          * for the unit, so the delayTime instance variable is always in msec.
444          * 
445          * @param initString
446          *            the string to initialize this Delay object from
447          **/
448         public Delay(String initString) throws MessagingException {
449             // Default unit value to 'msec'.
450             String unit = "msec";
451 
452             if (delayTimeMatcher.matches(initString, PATTERN)) {
453                 MatchResult res = delayTimeMatcher.getMatch();
454 
455                 // The capturing groups will now hold:
456                 // at 1: attempts * (if present)
457                 // at 2: delaytime
458                 // at 3: unit (if present)
459                 if (res.group(1) != null && !res.group(1).equals("")) {
460                     // We have an attempt *
461                     String attemptMatch = res.group(1);
462 
463                     // Strip the * and whitespace.
464                     attemptMatch = attemptMatch.substring(0,
465                             attemptMatch.length() - 1).trim();
466                     attempts = Integer.parseInt(attemptMatch);
467                 }
468 
469                 delayTime = Long.parseLong(res.group(2));
470 
471                 if (!res.group(3).equals("")) {
472                     // We have a value for 'unit'.
473                     unit = res.group(3).toLowerCase(Locale.US);
474                 }
475             } else {
476                 throw new MessagingException(initString + " does not match "
477                         + PATTERN_STRING);
478             }
479 
480             // Look for unit in the MULTIPLIERS Hashmap & calculate delayTime.
481             if (MULTIPLIERS.get(unit) != null) {
482                 int multiplier = ((Integer) MULTIPLIERS.get(unit)).intValue();
483                 delayTime *= multiplier;
484             } else {
485                 throw new MessagingException("Unknown unit: " + unit);
486             }
487         }
488 
489         /**
490          * This constructor makes a default Delay object with attempts = 1 and
491          * delayTime = DEFAULT_DELAY_TIME.
492          **/
493         public Delay() {
494         }
495 
496         /**
497          * @return the delayTime for this Delay
498          **/
499         public long getDelayTime() {
500             return delayTime;
501         }
502 
503         /**
504          * @return the number attempts this Delay should be used.
505          **/
506         public int getAttempts() {
507             return attempts;
508         }
509 
510         /**
511          * Set the number attempts this Delay should be used.
512          **/
513         public void setAttempts(int value) {
514             attempts = value;
515         }
516 
517         /**
518          * Pretty prints this Delay
519          **/
520         public String toString() {
521             String message = getAttempts() + "*" + getDelayTime() + "msecs";
522             return message;
523         }
524     }
525 
526     public String getMailetInfo() {
527         return "Retry Mailet";
528     }
529 
530     /**
531      * Checks if maximum retry count has been reached. If it is, then it
532      * forwards the message to the error processor; otherwise writes it to the
533      * retry repository.
534      * 
535      * @param mail
536      *            the mail to be retried.
537      * @throws MessagingException
538      *             on failure to send it to the error processor.
539      * 
540      * @see org.apache.mailet.Mailet#service(org.apache.mailet.Mail)
541      */
542     public void service(Mail mail) throws MessagingException {
543         if (isDebug) {
544             log("Retrying mail " + mail.getName());
545         }
546 
547         // Save the original error message.
548         mail.setAttribute(ORIGINAL_ERROR, mail.getErrorMessage());
549 
550         // Get retry count and put it in the error message.
551         // Note: 'errorMessage' is the only argument of 'accept' method in
552         // SpoolRepository.AcceptFilter that can be used to pass the retry
553         // count.
554         String retryCount = (String) mail.getAttribute(RETRY_COUNT);
555         if (retryCount == null) {
556             retryCount = "0";
557         }
558         mail.setErrorMessage(retryCount);
559 
560         int retries = Integer.parseInt(retryCount);
561         String message = "";
562 
563         // If maximum retries number hasn't reached, store message in retrying
564         // repository.
565         if (retries < maxRetries) {
566             message = "Storing " + mail.getMessage().getMessageID()
567                     + " to retry repository " + workRepositoryPath
568                     + ", retry " + retries;
569             log(message);
570 
571             mail.setAttribute(RETRY_COUNT, retryCount);
572             workRepository.store(mail);
573             mail.setState(Mail.GHOST);
574         } else {
575             // Forward message to 'errorProcessor'.
576             message = "Sending " + mail.getMessage().getMessageID()
577                     + " to error processor after retrying " + retries
578                     + " times.";
579             log(message);
580             mail.setState(errorProcessor);
581             MailetContext mc = getMailetContext();
582             try {
583                 message = "Message failed after " + retries
584                         + " retries with error " + "message: "
585                         + mail.getAttribute(ORIGINAL_ERROR);
586                 mail.setErrorMessage(message);
587                 mc.sendMail(mail);
588             } catch (MessagingException e) {
589                 // We shouldn't get an exception, because the mail was already
590                 // processed.
591                 log("Exception re-inserting failed mail: ", e);
592                 throw new MessagingException(
593                         "Exception encountered while bouncing "
594                                 + "mail in Retry process.", e);
595             }
596         }
597     }
598     
599     /**
600      * Stops all the worker threads that are waiting for messages. This method is
601      * called by the Mailet container before taking this Mailet out of service.
602      */
603     public synchronized void destroy() {
604         // Mark flag so threads from this Mailet stop themselves
605         destroyed = true;
606 
607         // Wake up all threads from waiting for an accept
608         for (Iterator i = workersThreads.iterator(); i.hasNext(); ) {
609             Thread t = (Thread)i.next();
610             t.interrupt();
611         }
612         notifyAll();
613     }
614 
615     /**
616      * Handles checking the retrying spool for new mail and retrying them if
617      * there are ready for retrying.
618      */
619     public void run() {
620         try {
621             while (!Thread.interrupted() && !destroyed) {
622                 try {
623                     // Get the 'mail' object that is ready for retrying. If no
624                     // message is
625                     // ready, the 'accept' will block until message is ready.
626                     // The amount
627                     // of time to block is determined by the 'getWaitTime'
628                     // method of the
629                     // MultipleDelayFilter.
630                     Mail mail = workRepository.accept(delayFilter);
631                     String key = mail.getName();
632                     try {
633                         if (isDebug) {
634                             String message = Thread.currentThread().getName()
635                                     + " will process mail " + key;
636                             log(message);
637                         }
638 
639                         // Retry message
640                         if (retry(mail)) {
641                             // If retry attempt was successful, remove message.
642                             // ContainerUtil.dispose(mail);
643                             workRepository.remove(key);
644                         } else {
645                             // Something happened that will delay delivery.
646                             // Store it back in the retry repository.
647                             workRepository.store(mail);
648                             ContainerUtil.dispose(mail);
649 
650                             // This is an update, so we have to unlock and
651                             // notify or this mail is kept locked by this thread.
652                             workRepository.unlock(key);
653                             
654                             // Note: We do not notify because we updated an
655                             // already existing mail and we are now free to handle 
656                             // more mails.
657                             // Furthermore this mail should not be processed now
658                             // because we have a retry time scheduling.
659                         }
660                         
661                         // Clear the object handle to make sure it recycles
662                         // this object.
663                         mail = null;
664                     } catch (Exception e) {
665                         // Prevent unexpected exceptions from causing looping by
666                         // removing message from outgoing.
667                         // DO NOT CHANGE THIS to catch Error! For example, if
668                         // there were an OutOfMemory condition caused because 
669                         // something else in the server was abusing memory, we would 
670                         // not want to start purging the retrying spool!
671                         ContainerUtil.dispose(mail);
672                         workRepository.remove(key);
673                         throw e;
674                     }
675                 } catch (Throwable e) {
676                     if (!destroyed) {
677                         log("Exception caught in Retry.run()", e);
678                     }
679                 }
680             }
681         } finally {
682             // Restore the thread state to non-interrupted.
683             Thread.interrupted();
684         }
685     }
686 
687     /**
688      * Retries delivery of a {@link Mail}.
689      * 
690      * @param mail
691      *            mail to be retried.
692      * @return {@code true} if message was resent successfully else {@code
693      *         false}
694      */
695     private boolean retry(Mail mail) {
696         if (isDebug) {
697             log("Attempting to deliver " + mail.getName());
698         }
699 
700         // Update retry count
701         int retries = Integer.parseInt((String) mail.getAttribute(RETRY_COUNT));
702         ++retries;
703         mail.setErrorMessage(retries + "");
704         mail.setAttribute(RETRY_COUNT, String.valueOf(retries));
705         mail.setLastUpdated(new Date());
706 
707         // Call preprocessor
708         preprocess(mail);
709 
710         // Send it to 'retry' processor
711         mail.setState(retryProcessor);
712         MailetContext mc = getMailetContext();
713         try {
714             String message = "Retrying message "
715                     + mail.getMessage().getMessageID() + ".  Attempt #: "
716                     + retries;
717             log(message);
718             mc.sendMail(mail);
719         } catch (MessagingException e) {
720             // We shouldn't get an exception, because the mail was already
721             // processed
722             log("Exception while retrying message. ", e);
723             return false;
724         }
725         return true;
726     }
727 
728     /**
729      * Pre-processes the {@link Mail} object before resending.
730      * <p>
731      * This method can be used by subclasses to perform application specific
732      * processing on the Mail object, such as, adding and/or removing
733      * application specific Mail attributes etc. The default implementation
734      * leaves the Mail object intact.
735      * 
736      * @param mail
737      *            mail object that can be customized before resending.
738      */
739     protected void preprocess(Mail mail) {
740     }
741 }