001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.transport.mailets.smime;
019:
020: import org.apache.james.security.KeyHolder;
021: import org.apache.james.security.SMIMEAttributeNames;
022: import org.apache.mailet.GenericMailet;
023: import org.apache.mailet.Mail;
024: import org.apache.mailet.MailAddress;
025: import org.apache.mailet.RFC2822Headers;
026:
027: import javax.mail.MessagingException;
028: import javax.mail.Session;
029: import javax.mail.internet.InternetAddress;
030: import javax.mail.internet.MimeBodyPart;
031: import javax.mail.internet.MimeMessage;
032: import javax.mail.internet.MimeMultipart;
033: import javax.mail.internet.ParseException;
034:
035: import java.io.IOException;
036: import java.util.ArrayList;
037: import java.util.Collection;
038: import java.util.Enumeration;
039: import java.util.HashSet;
040: import java.util.Iterator;
041:
042: /**
043: * <P>Abstract mailet providing common SMIME signature services.<BR>
044: * It can be subclassed to make authoring signing mailets simple.<BR>
045: * By extending it and overriding one or more of the following methods a new behaviour can
046: * be quickly created without the author having to address any issue other than
047: * the relevant one:</P>
048: * <ul>
049: * <li>{@link #initDebug}, {@link #setDebug} and {@link #isDebug} manage the debugging mode.</li>
050: * <li>{@link #initExplanationText}, {@link #setExplanationText} and {@link #getExplanationText} manage the text of
051: * an attachment that will be added to explain the meaning of this server-side signature.</li>
052: * <li>{@link #initKeyHolder}, {@link #setKeyHolder} and {@link #getKeyHolder} manage the {@link KeyHolder} object that will
053: * contain the keys and certificates and will do the crypto work.</li>
054: * <li>{@link #initPostmasterSigns}, {@link #setPostmasterSigns} and {@link #isPostmasterSigns}
055: * determines whether messages originated by the Postmaster will be signed or not.</li>
056: * <li>{@link #initRebuildFrom}, {@link #setRebuildFrom} and {@link #isRebuildFrom}
057: * determines whether the "From:" header will be rebuilt to neutralize the wrong behaviour of
058: * some MUAs like Microsoft Outlook Express.</li>
059: * <li>{@link #initSignerName}, {@link #setSignerName} and {@link #getSignerName} manage the name
060: * of the signer to be shown in the explanation text.</li>
061: * <li>{@link #isOkToSign} controls whether the mail can be signed or not.</li>
062: * <li>The abstract method {@link #getWrapperBodyPart} returns the massaged {@link javax.mail.internet.MimeBodyPart}
063: * that will be signed, or null if the message has to be signed "as is".</li>
064: * </ul>
065: *
066: * <P>Handles the following init parameters:</P>
067: * <ul>
068: * <li><debug>: if <CODE>true</CODE> some useful information is logged.
069: * The default is <CODE>false</CODE>.</li>
070: * <li><keyStoreFileName>: the {@link java.security.KeyStore} full file name.</li>
071: * <li><keyStorePassword>: the <CODE>KeyStore</CODE> password.
072: * If given, it is used to check the integrity of the keystore data,
073: * otherwise, if null, the integrity of the keystore is not checked.</li>
074: * <li><keyAlias>: the alias name to use to search the Key using {@link java.security.KeyStore#getKey}.
075: * The default is to look for the first and only alias in the keystore;
076: * if zero or more than one is found a {@link java.security.KeyStoreException} is thrown.</li>
077: * <li><keyAliasPassword>: the alias password. The default is to use the <CODE>KeyStore</CODE> password.
078: * At least one of the passwords must be provided.</li>
079: * <li><keyStoreType>: the type of the keystore. The default will use {@link java.security.KeyStore#getDefaultType}.</li>
080: * <li><postmasterSigns>: if <CODE>true</CODE> the message will be signed even if the sender is the Postmaster.
081: * The default is <CODE>false</CODE>.</li></li>
082: * <li><rebuildFrom>: If <CODE>true</CODE> will modify the "From:" header.
083: * For more info see {@link #isRebuildFrom}.
084: * The default is <CODE>false</CODE>.</li>
085: * <li><signerName>: the name of the signer to be shown in the explanation text.
086: * The default is to use the "CN=" property of the signing certificate.</li>
087: * <li><explanationText>: the text of an explanation of the meaning of this server-side signature.
088: * May contain the following substitution patterns (see also {@link #getReplacedExplanationText}):
089: * <CODE>[signerName]</CODE>, <CODE>[signerAddress]</CODE>, <CODE>[reversePath]</CODE>, <CODE>[headers]</CODE>.
090: * It should be included in the signature.
091: * The actual presentation of the text depends on the specific concrete mailet subclass:
092: * see for example {@link SMIMESign}.
093: * The default is to not have any explanation text.</li>
094: * </ul>
095: * @version CVS $Revision: 494012 $ $Date: 2007-01-08 11:23:58 +0100 (Mo, 08 Jan 2007) $
096: * @since 2.2.1
097: */
098: public abstract class SMIMEAbstractSign extends GenericMailet {
099:
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
153: : new Boolean(getInitParameter("debug")).booleanValue());
154: }
155:
156: /**
157: * Getter for property debug.
158: * @return Value of property debug.
159: */
160: public boolean isDebug() {
161: return this .debug;
162: }
163:
164: /**
165: * Setter for property debug.
166: * @param debug New value of property debug.
167: */
168: public void setDebug(boolean debug) {
169: this .debug = debug;
170: }
171:
172: /**
173: * Initializer for property explanationText.
174: */
175: protected void initExplanationText() {
176: setExplanationText(getInitParameter("explanationText"));
177: if (isDebug()) {
178: log("Explanation text:\r\n" + getExplanationText());
179: }
180: }
181:
182: /**
183: * Getter for property explanationText.
184: * Text to be used in the SignatureExplanation.txt file.
185: * @return Value of property explanationText.
186: */
187: public String getExplanationText() {
188: return this .explanationText;
189: }
190:
191: /**
192: * Setter for property explanationText.
193: * @param explanationText New value of property explanationText.
194: */
195: public void setExplanationText(String explanationText) {
196: this .explanationText = explanationText;
197: }
198:
199: /**
200: * Initializer for property keyHolder.
201: */
202: protected void initKeyHolder() throws Exception {
203: String keyStoreFileName = getInitParameter("keyStoreFileName");
204: if (keyStoreFileName == null) {
205: throw new MessagingException(
206: "<keyStoreFileName> parameter missing.");
207: }
208:
209: String keyStorePassword = getInitParameter("keyStorePassword");
210: if (keyStorePassword == null) {
211: throw new MessagingException(
212: "<keyStorePassword> parameter missing.");
213: }
214: String keyAliasPassword = getInitParameter("keyAliasPassword");
215: if (keyAliasPassword == null) {
216: keyAliasPassword = keyStorePassword;
217: if (isDebug()) {
218: log("<keyAliasPassword> parameter not specified: will default to the <keyStorePassword> parameter.");
219: }
220: }
221:
222: String keyStoreType = getInitParameter("keyStoreType");
223: if (keyStoreType == null) {
224: if (isDebug()) {
225: log("<type> parameter not specified: will default to \""
226: + KeyHolder.getDefaultType() + "\".");
227: }
228: }
229:
230: String keyAlias = getInitParameter("keyAlias");
231: if (keyAlias == null) {
232: if (isDebug()) {
233: log("<keyAlias> parameter not specified: will look for the first one in the keystore.");
234: }
235: }
236:
237: if (isDebug()) {
238: StringBuffer logBuffer = new StringBuffer(1024).append(
239: "KeyStore related parameters:").append(
240: " keyStoreFileName=").append(keyStoreFileName)
241: .append(", keyStoreType=").append(keyStoreType)
242: .append(", keyAlias=").append(keyAlias).append(" ");
243: log(logBuffer.toString());
244: }
245:
246: // Certificate preparation
247: setKeyHolder(new KeyHolder(keyStoreFileName, keyStorePassword,
248: keyAlias, keyAliasPassword, keyStoreType));
249:
250: if (isDebug()) {
251: log("Subject Distinguished Name: "
252: + getKeyHolder().getSignerDistinguishedName());
253: }
254:
255: if (getKeyHolder().getSignerAddress() == null) {
256: throw new MessagingException(
257: "Signer address missing in the certificate.");
258: }
259: }
260:
261: /**
262: * Getter for property keyHolder.
263: * It is <CODE>protected</CODE> instead of <CODE>public</CODE> for security reasons.
264: * @return Value of property keyHolder.
265: */
266: protected KeyHolder getKeyHolder() {
267: return this .keyHolder;
268: }
269:
270: /**
271: * Setter for property keyHolder.
272: * It is <CODE>protected</CODE> instead of <CODE>public</CODE> for security reasons.
273: * @param keyHolder New value of property keyHolder.
274: */
275: protected void setKeyHolder(KeyHolder keyHolder) {
276: this .keyHolder = keyHolder;
277: }
278:
279: /**
280: * Initializer for property postmasterSigns.
281: */
282: protected void initPostmasterSigns() {
283: setPostmasterSigns((getInitParameter("postmasterSigns") == null) ? false
284: : new Boolean(getInitParameter("postmasterSigns"))
285: .booleanValue());
286: }
287:
288: /**
289: * Getter for property postmasterSigns.
290: * If true will sign messages signed by the postmaster.
291: * @return Value of property postmasterSigns.
292: */
293: public boolean isPostmasterSigns() {
294: return this .postmasterSigns;
295: }
296:
297: /**
298: * Setter for property postmasterSigns.
299: * @param postmasterSigns New value of property postmasterSigns.
300: */
301: public void setPostmasterSigns(boolean postmasterSigns) {
302: this .postmasterSigns = postmasterSigns;
303: }
304:
305: /**
306: * Initializer for property rebuildFrom.
307: */
308: protected void initRebuildFrom() throws MessagingException {
309: setRebuildFrom((getInitParameter("rebuildFrom") == null) ? false
310: : new Boolean(getInitParameter("rebuildFrom"))
311: .booleanValue());
312: if (isDebug()) {
313: if (isRebuildFrom()) {
314: log("Will modify the \"From:\" header.");
315: } else {
316: log("Will leave the \"From:\" header unchanged.");
317: }
318: }
319: }
320:
321: /**
322: * Getter for property rebuildFrom.
323: * If true will modify the "From:" header.
324: * <P>The modification is as follows:
325: * assuming that the signer mail address in the signer certificate is <I>trusted-server@xxx.com></I>
326: * and that <I>From: "John Smith" <john.smith@xxx.com></I>
327: * we will get <I>From: "John Smith" <john.smith@xxx.com>" <trusted-server@xxx.com></I>.</P>
328: * <P>If the "ReplyTo:" header is missing or empty it will be set to the original "From:" header.</P>
329: * <P>Such modification is necessary to achieve a correct behaviour
330: * with some mail clients (e.g. Microsoft Outlook Express).</P>
331: * @return Value of property rebuildFrom.
332: */
333: public boolean isRebuildFrom() {
334: return this .rebuildFrom;
335: }
336:
337: /**
338: * Setter for property rebuildFrom.
339: * @param rebuildFrom New value of property rebuildFrom.
340: */
341: public void setRebuildFrom(boolean rebuildFrom) {
342: this .rebuildFrom = rebuildFrom;
343: }
344:
345: /**
346: * Initializer for property signerName.
347: */
348: protected void initSignerName() {
349: setSignerName(getInitParameter("signerName"));
350: if (getSignerName() == null) {
351: if (getKeyHolder() == null) {
352: throw new RuntimeException(
353: "initKeyHolder() must be invoked before initSignerName()");
354: }
355: setSignerName(getKeyHolder().getSignerCN());
356: if (isDebug()) {
357: log("<signerName> parameter not specified: will use the certificate signer \"CN=\" attribute.");
358: }
359: }
360: }
361:
362: /**
363: * Getter for property signerName.
364: * @return Value of property signerName.
365: */
366: public String getSignerName() {
367: return this .signerName;
368: }
369:
370: /**
371: * Setter for property signerName.
372: * @param signerName New value of property signerName.
373: */
374: public void setSignerName(String signerName) {
375: this .signerName = signerName;
376: }
377:
378: /* ******************************************************************** */
379: /* ****************** End of setters and getters ********************** */
380: /* ******************************************************************** */
381:
382: /**
383: * Mailet initialization routine.
384: */
385: public void init() throws MessagingException {
386:
387: // check that all init parameters have been declared in allowedInitParameters
388: checkInitParameters(getAllowedInitParameters());
389:
390: try {
391: initDebug();
392: if (isDebug()) {
393: log("Initializing");
394: }
395:
396: initKeyHolder();
397: initSignerName();
398: initPostmasterSigns();
399: initRebuildFrom();
400: initExplanationText();
401:
402: } catch (MessagingException me) {
403: throw me;
404: } catch (Exception e) {
405: log("Exception thrown", e);
406: throw new MessagingException("Exception thrown", e);
407: } finally {
408: if (isDebug()) {
409: StringBuffer logBuffer = new StringBuffer(1024).append(
410: "Other parameters:").append(", signerName=")
411: .append(getSignerName()).append(
412: ", postmasterSigns=").append(
413: postmasterSigns).append(
414: ", rebuildFrom=").append(rebuildFrom)
415: .append(" ");
416: log(logBuffer.toString());
417: }
418: }
419:
420: }
421:
422: /**
423: * Service does the hard work, and signs
424: *
425: * @param mail the mail to sign
426: * @throws MessagingException if a problem arises signing the mail
427: */
428: public void service(Mail mail) throws MessagingException {
429:
430: try {
431: if (!isOkToSign(mail)) {
432: return;
433: }
434:
435: MimeBodyPart wrapperBodyPart = getWrapperBodyPart(mail);
436:
437: MimeMessage originalMessage = mail.getMessage();
438:
439: // do it
440: MimeMultipart signedMimeMultipart;
441: if (wrapperBodyPart != null) {
442: signedMimeMultipart = getKeyHolder().generate(
443: wrapperBodyPart);
444: } else {
445: signedMimeMultipart = getKeyHolder().generate(
446: originalMessage);
447: }
448:
449: MimeMessage newMessage = new MimeMessage(Session
450: .getDefaultInstance(System.getProperties(), null));
451: Enumeration headerEnum = originalMessage
452: .getAllHeaderLines();
453: while (headerEnum.hasMoreElements()) {
454: newMessage.addHeaderLine((String) headerEnum
455: .nextElement());
456: }
457:
458: newMessage.setSender(new InternetAddress(getKeyHolder()
459: .getSignerAddress(), getSignerName()));
460:
461: if (isRebuildFrom()) {
462: // builds a new "mixed" "From:" header
463: InternetAddress modifiedFromIA = new InternetAddress(
464: getKeyHolder().getSignerAddress(), mail
465: .getSender().toString());
466: newMessage.setFrom(modifiedFromIA);
467:
468: // if the original "ReplyTo:" header is missing sets it to the original "From:" header
469: newMessage.setReplyTo(originalMessage.getReplyTo());
470: }
471:
472: newMessage.setContent(signedMimeMultipart,
473: signedMimeMultipart.getContentType());
474: String messageId = originalMessage.getMessageID();
475: newMessage.saveChanges();
476: if (messageId != null) {
477: newMessage.setHeader(RFC2822Headers.MESSAGE_ID,
478: messageId);
479: }
480:
481: mail.setMessage(newMessage);
482:
483: // marks this mail as server-signed
484: mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNING_MAILET,
485: this .getClass().getName());
486: // it is valid for us by definition (signed here by us)
487: mail.setAttribute(
488: SMIMEAttributeNames.SMIME_SIGNATURE_VALIDITY,
489: "valid");
490:
491: // saves the trusted server signer address
492: // warning: should be same as the mail address in the certificate, but it is not guaranteed
493: mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNER_ADDRESS,
494: getKeyHolder().getSignerAddress());
495:
496: if (isDebug()) {
497: log("Message signed, reverse-path: " + mail.getSender()
498: + ", Id: " + messageId);
499: }
500:
501: } catch (MessagingException me) {
502: log("MessagingException found - could not sign!", me);
503: throw me;
504: } catch (Exception e) {
505: log("Exception found", e);
506: throw new MessagingException(
507: "Exception thrown - could not sign!", e);
508: }
509:
510: }
511:
512: /**
513: * <P>Checks if the mail can be signed.</P>
514: * <P>Rules:</P>
515: * <OL>
516: * <LI>The reverse-path != null (it is not a bounce).</LI>
517: * <LI>The sender user must have been SMTP authenticated.</LI>
518: * <LI>Either:</LI>
519: * <UL>
520: * <LI>The reverse-path is the postmaster address and {@link #isPostmasterSigns} returns <I>true</I></LI>
521: * <LI>or the reverse-path == the authenticated user
522: * and there is at least one "From:" address == reverse-path.</LI>.
523: * </UL>
524: * <LI>The message has not already been signed (mimeType != <I>multipart/signed</I>
525: * and != <I>application/pkcs7-mime</I>).</LI>
526: * </OL>
527: * @param mail The mail object to check.
528: * @return True if can be signed.
529: */
530: protected boolean isOkToSign(Mail mail) throws MessagingException {
531:
532: MailAddress reversePath = mail.getSender();
533:
534: // Is it a bounce?
535: if (reversePath == null) {
536: return false;
537: }
538:
539: String authUser = (String) mail
540: .getAttribute("org.apache.james.SMTPAuthUser");
541: // was the sender user SMTP authorized?
542: if (authUser == null) {
543: return false;
544: }
545:
546: // The sender is the postmaster?
547: if (getMailetContext().getPostmaster().equals(reversePath)) {
548: // should not sign postmaster sent messages?
549: if (!isPostmasterSigns()) {
550: return false;
551: }
552: } else {
553: // is the reverse-path user different from the SMTP authorized user?
554: if (!reversePath.getUser().equals(authUser)) {
555: return false;
556: }
557: // is there no "From:" address same as the reverse-path?
558: if (!fromAddressSameAsReverse(mail)) {
559: return false;
560: }
561: }
562:
563: // if already signed return false
564: MimeMessage mimeMessage = mail.getMessage();
565: if (mimeMessage.isMimeType("multipart/signed")
566: || mimeMessage.isMimeType("application/pkcs7-mime")) {
567: return false;
568: }
569:
570: return true;
571: }
572:
573: /**
574: * Creates the {@link javax.mail.internet.MimeBodyPart} that will be signed.
575: * For example, may attach a text file explaining the meaning of the signature,
576: * or an XML file containing information that can be checked by other MTAs.
577: * @param mail The mail to massage.
578: * @return The massaged MimeBodyPart to sign, or null to have the whole message signed "as is".
579: */
580: protected abstract MimeBodyPart getWrapperBodyPart(Mail mail)
581: throws MessagingException, IOException;
582:
583: /**
584: * Checks if there are unallowed init parameters specified in the configuration file
585: * against the String[] allowedInitParameters.
586: */
587: private void checkInitParameters(String[] allowedArray)
588: throws MessagingException {
589: // if null then no check is requested
590: if (allowedArray == null) {
591: return;
592: }
593:
594: Collection allowed = new HashSet();
595: Collection bad = new ArrayList();
596:
597: for (int i = 0; i < allowedArray.length; i++) {
598: allowed.add(allowedArray[i]);
599: }
600:
601: Iterator iterator = getInitParameterNames();
602: while (iterator.hasNext()) {
603: String parameter = (String) iterator.next();
604: if (!allowed.contains(parameter)) {
605: bad.add(parameter);
606: }
607: }
608:
609: if (bad.size() > 0) {
610: throw new MessagingException(
611: "Unexpected init parameters found: "
612: + arrayToString(bad.toArray()));
613: }
614: }
615:
616: /**
617: * Utility method for obtaining a string representation of an array of Objects.
618: */
619: private final String arrayToString(Object[] array) {
620: if (array == null) {
621: return "null";
622: }
623: StringBuffer sb = new StringBuffer(1024);
624: sb.append("[");
625: for (int i = 0; i < array.length; i++) {
626: if (i > 0) {
627: sb.append(",");
628: }
629: sb.append(array[i]);
630: }
631: sb.append("]");
632: return sb.toString();
633: }
634:
635: /**
636: * Utility method that checks if there is at least one address in the "From:" header
637: * same as the <i>reverse-path</i>.
638: * @param mail The mail to check.
639: * @return True if an address is found, false otherwise.
640: */
641: protected final boolean fromAddressSameAsReverse(Mail mail) {
642:
643: MailAddress reversePath = mail.getSender();
644:
645: if (reversePath == null) {
646: return false;
647: }
648:
649: try {
650: InternetAddress[] fromArray = (InternetAddress[]) mail
651: .getMessage().getFrom();
652: if (fromArray != null) {
653: for (int i = 0; i < fromArray.length; i++) {
654: MailAddress mailAddress = null;
655: try {
656: mailAddress = new MailAddress(fromArray[i]);
657: } catch (ParseException pe) {
658: log("Unable to parse a \"FROM\" header address: "
659: + fromArray[i].toString()
660: + "; ignoring.");
661: continue;
662: }
663: if (mailAddress.equals(reversePath)) {
664: return true;
665: }
666: }
667: }
668: } catch (MessagingException me) {
669: log("Unable to parse the \"FROM\" header; ignoring.");
670: }
671:
672: return false;
673:
674: }
675:
676: /**
677: * Utility method for obtaining a string representation of the Message's headers
678: * @param message The message to extract the headers from.
679: * @return The string containing the headers.
680: */
681: protected final String getMessageHeaders(MimeMessage message)
682: throws MessagingException {
683: Enumeration heads = message.getAllHeaderLines();
684: StringBuffer headBuffer = new StringBuffer(1024);
685: while (heads.hasMoreElements()) {
686: headBuffer.append(heads.nextElement().toString()).append(
687: "\r\n");
688: }
689: return headBuffer.toString();
690: }
691:
692: /**
693: * Prepares the explanation text making substitutions in the <I>explanationText</I> template string.
694: * Utility method that searches for all occurrences of some pattern strings
695: * and substitute them with the appropriate params.
696: * @param explanationText The template string for the explanation text.
697: * @param signerName The string that will replace the <CODE>[signerName]</CODE> pattern.
698: * @param signerAddress The string that will replace the <CODE>[signerAddress]</CODE> pattern.
699: * @param reversePath The string that will replace the <CODE>[reversePath]</CODE> pattern.
700: * @param headers The string that will replace the <CODE>[headers]</CODE> pattern.
701: * @return The actual explanation text string with all replacements done.
702: */
703: protected final String getReplacedExplanationText(
704: String explanationText, String signerName,
705: String signerAddress, String reversePath, String headers) {
706:
707: String replacedExplanationText = explanationText;
708:
709: replacedExplanationText = getReplacedString(
710: replacedExplanationText, SIGNER_NAME_PATTERN,
711: signerName);
712: replacedExplanationText = getReplacedString(
713: replacedExplanationText, SIGNER_ADDRESS_PATTERN,
714: signerAddress);
715: replacedExplanationText = getReplacedString(
716: replacedExplanationText, REVERSE_PATH_PATTERN,
717: reversePath);
718: replacedExplanationText = getReplacedString(
719: replacedExplanationText, HEADERS_PATTERN, headers);
720:
721: return replacedExplanationText;
722: }
723:
724: /**
725: * Searches the <I>template</I> String for all occurrences of the <I>pattern</I> string
726: * and creates a new String substituting them with the <I>actual</I> String.
727: * @param template The template String to work on.
728: * @param pattern The string to search for the replacement.
729: * @param actual The actual string to use for the replacement.
730: */
731: private String getReplacedString(String template, String pattern,
732: String actual) {
733: if (actual != null) {
734: StringBuffer sb = new StringBuffer(template.length());
735: int fromIndex = 0;
736: int index;
737: while ((index = template.indexOf(pattern, fromIndex)) >= 0) {
738: sb.append(template.substring(fromIndex, index));
739: sb.append(actual);
740: fromIndex = index + pattern.length();
741: }
742: if (fromIndex < template.length()) {
743: sb.append(template.substring(fromIndex));
744: }
745: return sb.toString();
746: } else {
747: return new String(template);
748: }
749: }
750:
751: }
|