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