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><debug>: if <CODE>true</CODE> some useful information is logged.
69 * The default is <CODE>false</CODE>.</li>
70 * <li><keyStoreFileName>: the {@link java.security.KeyStore} full file name.</li>
71 * <li><keyStorePassword>: 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><keyAlias>: 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><keyAliasPassword>: 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><keyStoreType>: the type of the keystore. The default will use {@link java.security.KeyStore#getDefaultType}.</li>
80 * <li><postmasterSigns>: 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><rebuildFrom>: 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><signerName>: 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><explanationText>: 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
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
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></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>" <trusted-server@xxx.com></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
370
371
372 /***
373 * Mailet initialization routine.
374 */
375 public void init() throws MessagingException {
376
377
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
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
449 InternetAddress modifiedFromIA = new InternetAddress(getKeyHolder().getSignerAddress(), mail.getSender().toString());
450 newMessage.setFrom(modifiedFromIA);
451
452
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
466 mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNING_MAILET, this.getClass().getName());
467
468 mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNATURE_VALIDITY, "valid");
469
470
471
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
512 if (reversePath == null) {
513 return false;
514 }
515
516 String authUser = (String) mail.getAttribute("org.apache.james.SMTPAuthUser");
517
518 if (authUser == null) {
519 return false;
520 }
521
522
523 if (getMailetContext().getPostmaster().equals(reversePath)) {
524
525 if (!isPostmasterSigns()) {
526 return false;
527 }
528 } else {
529
530 if (!reversePath.getUser().equals(authUser)) {
531 return false;
532 }
533
534 if (!fromAddressSameAsReverse(mail)) {
535 return false;
536 }
537 }
538
539
540
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
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