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.mailet.crypto.mailet;
23  
24  import org.apache.james.mailet.crypto.KeyHolder;
25  import org.apache.james.mailet.crypto.SMIMEAttributeNames;
26  import org.apache.mailet.base.GenericMailet;
27  import org.apache.mailet.Mail;
28  import org.apache.mailet.MailAddress;
29  import org.apache.mailet.base.RFC2822Headers;
30  
31  import javax.mail.MessagingException;
32  import javax.mail.Session;
33  import javax.mail.internet.InternetAddress;
34  import javax.mail.internet.MimeBodyPart;
35  import javax.mail.internet.MimeMessage;
36  import javax.mail.internet.MimeMultipart;
37  import javax.mail.internet.ParseException;
38  
39  import java.io.IOException;
40  import java.util.Enumeration;
41  import java.lang.reflect.Constructor;
42  
43  /**
44   * <P>Abstract mailet providing common SMIME signature services.
45   * It can be subclassed to make authoring signing mailets simple.
46   * By extending it and overriding one or more of the following methods a new behaviour can
47   * be quickly created without the author having to address any issue other than
48   * the relevant one:</P>
49   * <ul>
50   * <li>{@link #initDebug}, {@link #setDebug} and {@link #isDebug} manage the debugging mode.</li>
51   * <li>{@link #initExplanationText}, {@link #setExplanationText} and {@link #getExplanationText} manage the text of
52   * an attachment that will be added to explain the meaning of this server-side signature.</li>
53   * <li>{@link #initKeyHolder}, {@link #setKeyHolder} and {@link #getKeyHolder} manage the {@link KeyHolder} object that will
54   * contain the keys and certificates and will do the crypto work.</li>
55   * <li>{@link #initPostmasterSigns}, {@link #setPostmasterSigns} and {@link #isPostmasterSigns}
56   * determines whether messages originated by the Postmaster will be signed or not.</li>
57   * <li>{@link #initRebuildFrom}, {@link #setRebuildFrom} and {@link #isRebuildFrom}
58   * determines whether the "From:" header will be rebuilt to neutralize the wrong behaviour of
59   * some MUAs like Microsoft Outlook Express.</li>
60   * <li>{@link #initSignerName}, {@link #setSignerName} and {@link #getSignerName} manage the name
61   * of the signer to be shown in the explanation text.</li>
62   * <li>{@link #isOkToSign} controls whether the mail can be signed or not.</li>
63   * <li>The abstract method {@link #getWrapperBodyPart} returns the massaged {@link javax.mail.internet.MimeBodyPart}
64   * that will be signed, or null if the message has to be signed "as is".</li>
65   * </ul>
66   *
67   * <P>Handles the following init parameters:</P>
68   * <ul>
69   * <li>&lt;keyHolderClass&gt;: Sets the class of the KeyHolder object that will handle the cryptography functions,
70   * for example org.apache.james.security.SMIMEKeyHolder for SMIME.</li>
71   * <li>&lt;debug&gt;: if <CODE>true</CODE> some useful information is logged.
72   * The default is <CODE>false</CODE>.</li>
73   * <li>&lt;keyStoreFileName&gt;: the {@link java.security.KeyStore} full file name.</li>
74   * <li>&lt;keyStorePassword&gt;: the <CODE>KeyStore</CODE> password.
75   *      If given, it is used to check the integrity of the keystore data,
76   *      otherwise, if null, the integrity of the keystore is not checked.</li>
77   * <li>&lt;keyAlias&gt;: the alias name to use to search the Key using {@link java.security.KeyStore#getKey}.
78   * The default is to look for the first and only alias in the keystore;
79   * if zero or more than one is found a {@link java.security.KeyStoreException} is thrown.</li>
80   * <li>&lt;keyAliasPassword&gt;: the alias password. The default is to use the <CODE>KeyStore</CODE> password.
81   *      At least one of the passwords must be provided.</li>
82   * <li>&lt;keyStoreType&gt;: the type of the keystore. The default will use {@link java.security.KeyStore#getDefaultType}.</li>
83   * <li>&lt;postmasterSigns&gt;: if <CODE>true</CODE> the message will be signed even if the sender is the Postmaster.
84   * The default is <CODE>false</CODE>.</li></li>
85   * <li>&lt;rebuildFrom&gt;: If <CODE>true</CODE> will modify the "From:" header.
86   * For more info see {@link #isRebuildFrom}.
87   * The default is <CODE>false</CODE>.</li>
88   * <li>&lt;signerName&gt;: the name of the signer to be shown in the explanation text.
89   * The default is to use the "CN=" property of the signing certificate.</li>
90   * <li>&lt;explanationText&gt;: the text of an explanation of the meaning of this server-side signature.
91   * May contain the following substitution patterns (see also {@link #getReplacedExplanationText}):
92   * <CODE>[signerName]</CODE>, <CODE>[signerAddress]</CODE>, <CODE>[reversePath]</CODE>, <CODE>[headers]</CODE>.
93   * It should be included in the signature.
94   * The actual presentation of the text depends on the specific concrete mailet subclass:
95   * see for example {@link SMIMESign}.
96   * The default is to not have any explanation text.</li>
97   * </ul>
98   * @version CVS $Revision: 744744 $ $Date: 2009-02-15 20:19:56 +0000 (Sun, 15 Feb 2009) $
99   * @since 2.2.1
100  */
101 public abstract class AbstractSign extends GenericMailet {
102     
103     private static final String HEADERS_PATTERN = "[headers]";
104     
105     private static final String SIGNER_NAME_PATTERN = "[signerName]";
106     
107     private static final String SIGNER_ADDRESS_PATTERN = "[signerAddress]";
108     
109     private static final String REVERSE_PATH_PATTERN = "[reversePath]";
110     
111     /**
112      * Holds value of property debug.
113      */
114     private boolean debug;
115     
116     /**
117      * Holds value of property keyHolderClass.
118      */
119     private Class keyHolderClass;
120     
121     /**
122      * Holds value of property explanationText.
123      */
124     private String explanationText;
125     
126     /**
127      * Holds value of property keyHolder.
128      */
129     private KeyHolder keyHolder;
130     
131     /**
132      * Holds value of property postmasterSigns.
133      */
134     private boolean postmasterSigns;
135     
136     /**
137      * Holds value of property rebuildFrom.
138      */
139     private boolean rebuildFrom;
140     
141     /**
142      * Holds value of property signerName.
143      */
144     private String signerName;
145     
146     /**
147      * Gets the expected init parameters.
148      * @return An array containing the parameter names allowed for this mailet.
149      */
150     protected abstract String[] getAllowedInitParameters();
151     
152     /* ******************************************************************** */
153     /* ****************** Begin of setters and getters ******************** */
154     /* ******************************************************************** */
155     
156     /**
157      * Initializer for property debug.
158      */
159     protected void initDebug() {
160         setDebug((getInitParameter("debug") == null) ? false : new Boolean(getInitParameter("debug")).booleanValue());
161     }
162     
163     /**
164      * Getter for property debug.
165      * @return Value of property debug.
166      */
167     public boolean isDebug() {
168         return this.debug;
169     }
170     
171     /**
172      * Setter for property debug.
173      * @param debug New value of property debug.
174      */
175     public void setDebug(boolean debug) {
176         this.debug = debug;
177     }
178     
179     /**
180      * Initializer for property keyHolderClass.
181      */
182     protected void initKeyHolderClass() throws MessagingException {
183         String keyHolderClassName = getInitParameter("keyHolderClass");
184         if (keyHolderClassName == null) {
185             throw new MessagingException("<keyHolderClass> parameter missing.");
186         }
187         try {
188             setKeyHolderClass(Class.forName(keyHolderClassName));
189         } catch (ClassNotFoundException cnfe) {
190             throw new MessagingException("The specified <keyHolderClass> does not exist: " + keyHolderClassName);
191         }
192         if (isDebug()) {
193             log("keyHolderClass: " + getKeyHolderClass());
194         }
195     }
196     
197     /**
198      * Getter for property keyHolderClass.
199      * @return Value of property keyHolderClass.
200      */
201     public Class getKeyHolderClass() {
202         return this.keyHolderClass;
203     }
204     
205     /**
206      * Setter for property keyHolderClass.
207      * @param keyHolderClass New value of property keyHolderClass.
208      */
209     public void setKeyHolderClass(Class keyHolderClass) {
210         this.keyHolderClass = keyHolderClass;
211     }
212     
213     /**
214      * Initializer for property explanationText.
215      */
216     protected void initExplanationText() {
217         setExplanationText(getInitParameter("explanationText"));
218         if (isDebug()) {
219             log("Explanation text:\r\n" + getExplanationText());
220         }
221     }
222     
223     /**
224      * Getter for property explanationText.
225      * Text to be used in the SignatureExplanation.txt file.
226      * @return Value of property explanationText.
227      */
228     public String getExplanationText() {
229         return this.explanationText;
230     }
231     
232     /**
233      * Setter for property explanationText.
234      * @param explanationText New value of property explanationText.
235      */
236     public void setExplanationText(String explanationText) {
237         this.explanationText = explanationText;
238     }
239     
240     /**
241      * Initializer for property keyHolder.
242      */
243     protected void initKeyHolder() throws Exception {
244         Constructor keyHolderConstructor = null;
245         try {
246             keyHolderConstructor = keyHolderClass.getConstructor(new Class[] {String.class, String.class, String.class, String.class, String.class});
247         } catch (NoSuchMethodException nsme) {
248             throw new MessagingException("The needed constructor does not exist: "
249                     + keyHolderClass + "(String, String, String, String, String)");
250         }
251         
252         
253         String keyStoreFileName = getInitParameter("keyStoreFileName");
254         if (keyStoreFileName == null) {
255             throw new MessagingException("<keyStoreFileName> parameter missing.");
256         }
257         
258         String keyStorePassword = getInitParameter("keyStorePassword");
259         if (keyStorePassword == null) {
260             throw new MessagingException("<keyStorePassword> parameter missing.");
261         }
262         String keyAliasPassword = getInitParameter("keyAliasPassword");
263         if (keyAliasPassword == null) {
264             keyAliasPassword = keyStorePassword;
265             if (isDebug()) {
266                 log("<keyAliasPassword> parameter not specified: will default to the <keyStorePassword> parameter.");
267             }
268         }
269         
270         String keyStoreType = getInitParameter("keyStoreType");
271         if (keyStoreType == null) {
272             if (isDebug()) {
273                 log("<keyStoreType> parameter not specified: the default will be as appropriate to the keyStore requested.");
274             }
275         }
276         
277         String keyAlias = getInitParameter("keyAlias");
278         if (keyAlias == null) {
279             if (isDebug()) {
280                 log("<keyAlias> parameter not specified: will look for the first one in the keystore.");
281             }
282         }
283         
284         if (isDebug()) {
285             StringBuffer logBuffer =
286             new StringBuffer(1024)
287             .append("KeyStore related parameters:")
288             .append("  keyStoreFileName=").append(keyStoreFileName)
289             .append(", keyStoreType=").append(keyStoreType)
290             .append(", keyAlias=").append(keyAlias)
291             .append(" ");
292             log(logBuffer.toString());
293         }
294             
295         // Certificate preparation
296         String[] parameters = {keyStoreFileName, keyStorePassword, keyAlias, keyAliasPassword, keyStoreType};
297         setKeyHolder((KeyHolder)keyHolderConstructor.newInstance(parameters));
298         
299         if (isDebug()) {
300             log("Subject Distinguished Name: " + getKeyHolder().getSignerDistinguishedName());
301         }
302         
303         if (getKeyHolder().getSignerAddress() == null) {
304             throw new MessagingException("Signer address missing in the certificate.");
305         }
306     }
307     
308     /**
309      * Getter for property keyHolder.
310      * It is <CODE>protected</CODE> instead of <CODE>public</CODE> for security reasons.
311      * @return Value of property keyHolder.
312      */
313     protected KeyHolder getKeyHolder() {
314         return this.keyHolder;
315     }
316     
317     /**
318      * Setter for property keyHolder.
319      * It is <CODE>protected</CODE> instead of <CODE>public</CODE> for security reasons.
320      * @param keyHolder New value of property keyHolder.
321      */
322     protected void setKeyHolder(KeyHolder keyHolder) {
323         this.keyHolder = keyHolder;
324     }
325     
326     /**
327      * Initializer for property postmasterSigns.
328      */
329     protected void initPostmasterSigns() {
330         setPostmasterSigns((getInitParameter("postmasterSigns") == null) ? false : new Boolean(getInitParameter("postmasterSigns")).booleanValue());
331     }
332     
333     /**
334      * Getter for property postmasterSigns.
335      * If true will sign messages signed by the postmaster.
336      * @return Value of property postmasterSigns.
337      */
338     public boolean isPostmasterSigns() {
339         return this.postmasterSigns;
340     }
341     
342     /**
343      * Setter for property postmasterSigns.
344      * @param postmasterSigns New value of property postmasterSigns.
345      */
346     public void setPostmasterSigns(boolean postmasterSigns) {
347         this.postmasterSigns = postmasterSigns;
348     }
349     
350     /**
351      * Initializer for property rebuildFrom.
352      */
353     protected void initRebuildFrom() throws MessagingException {
354         setRebuildFrom((getInitParameter("rebuildFrom") == null) ? false : new Boolean(getInitParameter("rebuildFrom")).booleanValue());
355         if (isDebug()) {
356             if (isRebuildFrom()) {
357                 log("Will modify the \"From:\" header.");
358             } else {
359                 log("Will leave the \"From:\" header unchanged.");
360             }
361         }
362     }
363     
364     /**
365      * Getter for property rebuildFrom.
366      * If true will modify the "From:" header.
367      * <P>The modification is as follows:
368      * assuming that the signer mail address in the signer certificate is <I>trusted-server@xxx.com&gt;</I>
369      * and that <I>From: "John Smith" <john.smith@xxx.com></I>
370      * we will get <I>From: "John Smith" <john.smith@xxx.com>" &lt;trusted-server@xxx.com&gt;</I>.</P>
371      * <P>If the "ReplyTo:" header is missing or empty it will be set to the original "From:" header.</P>
372      * <P>Such modification is necessary to achieve a correct behaviour
373      * with some mail clients (e.g. Microsoft Outlook Express).</P>
374      * @return Value of property rebuildFrom.
375      */
376     public boolean isRebuildFrom() {
377         return this.rebuildFrom;
378     }
379     
380     /**
381      * Setter for property rebuildFrom.
382      * @param rebuildFrom New value of property rebuildFrom.
383      */
384     public void setRebuildFrom(boolean rebuildFrom) {
385         this.rebuildFrom = rebuildFrom;
386     }
387     
388     /**
389      * Initializer for property signerName.
390      */
391     protected void initSignerName() {
392         setSignerName(getInitParameter("signerName"));
393         if (getSignerName() == null) {
394             if (getKeyHolder() == null) {
395                 throw new RuntimeException("initKeyHolder() must be invoked before initSignerName()");
396             }
397             setSignerName(getKeyHolder().getSignerCN());
398             if (isDebug()) {
399                 log("<signerName> parameter not specified: will use the certificate signer \"CN=\" attribute.");
400             }
401         }
402     }
403     
404     /**
405      * Getter for property signerName.
406      * @return Value of property signerName.
407      */
408     public String getSignerName() {
409         return this.signerName;
410     }
411     
412     /**
413      * Setter for property signerName.
414      * @param signerName New value of property signerName.
415      */
416     public void setSignerName(String signerName) {
417         this.signerName = signerName;
418     }
419     
420     /* ******************************************************************** */
421     /* ****************** End of setters and getters ********************** */
422     /* ******************************************************************** */    
423     
424     /**
425      * Mailet initialization routine.
426      */
427     public void init() throws MessagingException {
428         
429         // check that all init parameters have been declared in allowedInitParameters
430         checkInitParameters(getAllowedInitParameters());
431         
432         try {
433             initDebug();
434             if (isDebug()) {
435                 log("Initializing");
436             }
437             
438             initKeyHolderClass();
439             initKeyHolder();
440             initSignerName();
441             initPostmasterSigns();
442             initRebuildFrom();
443             initExplanationText();
444             
445             
446         } catch (MessagingException me) {
447             throw me;
448         } catch (Exception e) {
449             log("Exception thrown", e);
450             throw new MessagingException("Exception thrown", e);
451         } finally {
452             if (isDebug()) {
453                 StringBuffer logBuffer =
454                 new StringBuffer(1024)
455                 .append("Other parameters:")
456                 .append(", signerName=").append(getSignerName())
457                 .append(", postmasterSigns=").append(postmasterSigns)
458                 .append(", rebuildFrom=").append(rebuildFrom)
459                 .append(" ");
460                 log(logBuffer.toString());
461             }
462         }
463         
464     }
465     
466     /**
467      * Service does the hard work, and signs
468      *
469      * @param mail the mail to sign
470      * @throws MessagingException if a problem arises signing the mail
471      */
472     public void service(Mail mail) throws MessagingException {
473         
474         try {
475             if (!isOkToSign(mail)) {
476                 return;
477             }
478             
479             MimeBodyPart wrapperBodyPart = getWrapperBodyPart(mail);
480             
481             MimeMessage originalMessage = mail.getMessage();
482             
483             // do it
484             MimeMultipart signedMimeMultipart;
485             if (wrapperBodyPart != null) {
486                 signedMimeMultipart = getKeyHolder().generate(wrapperBodyPart);
487             } else {
488                 signedMimeMultipart = getKeyHolder().generate(originalMessage);
489             }
490             
491             MimeMessage newMessage = new MimeMessage(Session.getDefaultInstance(System.getProperties(),
492             null));
493             Enumeration headerEnum = originalMessage.getAllHeaderLines();
494             while (headerEnum.hasMoreElements()) {
495                 newMessage.addHeaderLine((String) headerEnum.nextElement());
496             }
497             
498             newMessage.setSender(new InternetAddress(getKeyHolder().getSignerAddress(), getSignerName()));
499   
500             if (isRebuildFrom()) {
501                 // builds a new "mixed" "From:" header
502                 InternetAddress modifiedFromIA = new InternetAddress(getKeyHolder().getSignerAddress(), mail.getSender().toString());
503                 newMessage.setFrom(modifiedFromIA);
504                 
505                 // if the original "ReplyTo:" header is missing sets it to the original "From:" header
506                 newMessage.setReplyTo(originalMessage.getReplyTo());
507             }
508             
509             newMessage.setContent(signedMimeMultipart, signedMimeMultipart.getContentType());
510             String messageId = originalMessage.getMessageID();
511             newMessage.saveChanges();
512             if (messageId != null) {
513                 newMessage.setHeader(RFC2822Headers.MESSAGE_ID, messageId);
514             }
515             
516             mail.setMessage(newMessage);
517             
518             // marks this mail as server-signed
519             mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNING_MAILET, this.getClass().getName());
520             // it is valid for us by definition (signed here by us)
521             mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNATURE_VALIDITY, "valid");
522             
523             // saves the trusted server signer address
524             // warning: should be same as the mail address in the certificate, but it is not guaranteed
525             mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNER_ADDRESS, getKeyHolder().getSignerAddress());
526             
527             if (isDebug()) {
528                 log("Message signed, reverse-path: " + mail.getSender() + ", Id: " + messageId);
529             }
530             
531         } catch (MessagingException me) {
532             log("MessagingException found - could not sign!", me);
533             throw me;
534         } catch (Exception e) {
535             log("Exception found", e);
536             throw new MessagingException("Exception thrown - could not sign!", e);
537         }
538         
539     }
540     
541     
542     /**
543      * <P>Checks if the mail can be signed.</P>
544      * <P>Rules:</P>
545      * <OL>
546      * <LI>The reverse-path != null (it is not a bounce).</LI>
547      * <LI>The sender user must have been SMTP authenticated.</LI>
548      * <LI>Either:</LI>
549      * <UL>
550      * <LI>The reverse-path is the postmaster address and {@link #isPostmasterSigns} returns <I>true</I></LI>
551      * <LI>or the reverse-path == the authenticated user
552      * and there is at least one "From:" address == reverse-path.</LI>.
553      * </UL>
554      * <LI>The message has not already been signed (mimeType != <I>multipart/signed</I>
555      * and != <I>application/pkcs7-mime</I>).</LI>
556      * </OL>
557      * @param mail The mail object to check.
558      * @return True if can be signed.
559      */
560     protected boolean isOkToSign(Mail mail) throws MessagingException {
561 
562         MailAddress reversePath = mail.getSender();
563         
564         // Is it a bounce?
565         if (reversePath == null) {
566             return false;
567         }
568         
569         String authUser = (String) mail.getAttribute("org.apache.james.SMTPAuthUser");
570         // was the sender user SMTP authorized?
571         if (authUser == null) {
572             return false;
573         }
574         
575         // The sender is the postmaster?
576         if (getMailetContext().getPostmaster().equals(reversePath)) {
577             // should not sign postmaster sent messages?
578             if (!isPostmasterSigns()) {
579                 return false;
580             }
581         } else {
582             // is the reverse-path user different from the SMTP authorized user?
583             if (!reversePath.getUser().equals(authUser)) {
584                 return false;
585             }
586             // is there no "From:" address same as the reverse-path?
587             if (!fromAddressSameAsReverse(mail)) {
588                 return false;
589             }
590         }
591         
592         
593         // if already signed return false
594         MimeMessage mimeMessage = mail.getMessage();
595         if (mimeMessage.isMimeType("multipart/signed")
596             || mimeMessage.isMimeType("application/pkcs7-mime")) {
597             return false;
598         }
599         
600         return true;
601     }
602     
603     /**
604      * Creates the {@link javax.mail.internet.MimeBodyPart} that will be signed.
605      * For example, may attach a text file explaining the meaning of the signature,
606      * or an XML file containing information that can be checked by other MTAs.
607      * @param mail The mail to massage.
608      * @return The massaged MimeBodyPart to sign, or null to have the whole message signed "as is".
609      */    
610     protected abstract MimeBodyPart getWrapperBodyPart(Mail mail) throws MessagingException, IOException;
611     
612     /**
613      * Utility method that checks if there is at least one address in the "From:" header
614      * same as the <i>reverse-path</i>.
615      * @param mail The mail to check.
616      * @return True if an address is found, false otherwise.
617      */    
618     protected final boolean fromAddressSameAsReverse(Mail mail) {
619         
620         MailAddress reversePath = mail.getSender();
621         
622         if (reversePath == null) {
623             return false;
624         }
625         
626         try {
627             InternetAddress[] fromArray = (InternetAddress[]) mail.getMessage().getFrom();
628             if (fromArray != null) {
629                 for (int i = 0; i < fromArray.length; i++) {
630                     MailAddress mailAddress  = null;
631                     try {
632                         mailAddress = new MailAddress(fromArray[i]);
633                     } catch (ParseException pe) {
634                         log("Unable to parse a \"FROM\" header address: " + fromArray[i].toString() + "; ignoring.");
635                         continue;
636                     }
637                     if (mailAddress.equals(reversePath)) {
638                         return true;
639                     }
640                 }
641             }
642         } catch (MessagingException me) {
643             log("Unable to parse the \"FROM\" header; ignoring.");
644         }
645         
646         return false;
647         
648     }
649     
650     /**
651      * Utility method for obtaining a string representation of the Message's headers
652      * @param message The message to extract the headers from.
653      * @return The string containing the headers.
654      */
655     protected final String getMessageHeaders(MimeMessage message) throws MessagingException {
656         Enumeration heads = message.getAllHeaderLines();
657         StringBuffer headBuffer = new StringBuffer(1024);
658         while(heads.hasMoreElements()) {
659             headBuffer.append(heads.nextElement().toString()).append("\r\n");
660         }
661         return headBuffer.toString();
662     }
663     
664     /**
665      * Prepares the explanation text making substitutions in the <I>explanationText</I> template string.
666      * Utility method that searches for all occurrences of some pattern strings
667      * and substitute them with the appropriate params.
668      * @param explanationText The template string for the explanation text.
669      * @param signerName The string that will replace the <CODE>[signerName]</CODE> pattern.
670      * @param signerAddress The string that will replace the <CODE>[signerAddress]</CODE> pattern.
671      * @param reversePath The string that will replace the <CODE>[reversePath]</CODE> pattern.
672      * @param headers The string that will replace the <CODE>[headers]</CODE> pattern.
673      * @return The actual explanation text string with all replacements done.
674      */    
675     protected final String getReplacedExplanationText(String explanationText, String signerName,
676     String signerAddress, String reversePath, String headers) {
677         
678         String replacedExplanationText = explanationText;
679         
680         replacedExplanationText = getReplacedString(replacedExplanationText, SIGNER_NAME_PATTERN, signerName);
681         replacedExplanationText = getReplacedString(replacedExplanationText, SIGNER_ADDRESS_PATTERN, signerAddress);
682         replacedExplanationText = getReplacedString(replacedExplanationText, REVERSE_PATH_PATTERN, reversePath);
683         replacedExplanationText = getReplacedString(replacedExplanationText, HEADERS_PATTERN, headers);
684         
685         return replacedExplanationText;
686     }
687     
688     /**
689      * Searches the <I>template</I> String for all occurrences of the <I>pattern</I> string
690      * and creates a new String substituting them with the <I>actual</I> String.
691      * @param template The template String to work on.
692      * @param pattern The string to search for the replacement.
693      * @param actual The actual string to use for the replacement.
694      */    
695     private String getReplacedString(String template, String pattern, String actual) {
696          if (actual != null) {
697              StringBuffer sb = new StringBuffer(template.length());
698             int fromIndex = 0;
699             int index;
700             while ((index = template.indexOf(pattern, fromIndex)) >= 0) {
701                 sb.append(template.substring(fromIndex, index));
702                 sb.append(actual);
703                 fromIndex = index + pattern.length();
704             }
705             if (fromIndex < template.length()){
706                 sb.append(template.substring(fromIndex));
707             }
708             return sb.toString();
709         } else {
710             return new String(template);
711         }
712     }
713     
714 }
715