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><keyHolderClass>: 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><debug>: if <CODE>true</CODE> some useful information is logged.
72 * The default is <CODE>false</CODE>.</li>
73 * <li><keyStoreFileName>: the {@link java.security.KeyStore} full file name.</li>
74 * <li><keyStorePassword>: 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><keyAlias>: 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><keyAliasPassword>: 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><keyStoreType>: the type of the keystore. The default will use {@link java.security.KeyStore#getDefaultType}.</li>
83 * <li><postmasterSigns>: 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><rebuildFrom>: 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><signerName>: 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><explanationText>: 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></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>" <trusted-server@xxx.com></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