View Javadoc

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