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 }