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