001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.mail.javamail; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.io.UnsupportedEncodingException; 024import java.util.Date; 025 026import javax.activation.DataHandler; 027import javax.activation.DataSource; 028import javax.activation.FileDataSource; 029import javax.activation.FileTypeMap; 030import javax.mail.BodyPart; 031import javax.mail.Message; 032import javax.mail.MessagingException; 033import javax.mail.internet.AddressException; 034import javax.mail.internet.InternetAddress; 035import javax.mail.internet.MimeBodyPart; 036import javax.mail.internet.MimeMessage; 037import javax.mail.internet.MimeMultipart; 038import javax.mail.internet.MimePart; 039import javax.mail.internet.MimeUtility; 040 041import org.springframework.core.io.InputStreamSource; 042import org.springframework.core.io.Resource; 043import org.springframework.lang.Nullable; 044import org.springframework.util.Assert; 045 046/** 047 * Helper class for populating a {@link javax.mail.internet.MimeMessage}. 048 * 049 * <p>Mirrors the simple setters of {@link org.springframework.mail.SimpleMailMessage}, 050 * directly applying the values to the underlying MimeMessage. Allows for defining 051 * a character encoding for the entire message, automatically applied by all methods 052 * of this helper class. 053 * 054 * <p>Offers support for HTML text content, inline elements such as images, and typical 055 * mail attachments. Also supports personal names that accompany mail addresses. Note that 056 * advanced settings can still be applied directly to the underlying MimeMessage object! 057 * 058 * <p>Typically used in {@link MimeMessagePreparator} implementations or 059 * {@link JavaMailSender} client code: simply instantiating it as a MimeMessage wrapper, 060 * invoking setters on the wrapper, using the underlying MimeMessage for mail sending. 061 * Also used internally by {@link JavaMailSenderImpl}. 062 * 063 * <p>Sample code for an HTML mail with an inline image and a PDF attachment: 064 * 065 * <pre class="code"> 066 * mailSender.send(new MimeMessagePreparator() { 067 * public void prepare(MimeMessage mimeMessage) throws MessagingException { 068 * MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true, "UTF-8"); 069 * message.setFrom("me@mail.com"); 070 * message.setTo("you@mail.com"); 071 * message.setSubject("my subject"); 072 * message.setText("my text <img src='cid:myLogo'>", true); 073 * message.addInline("myLogo", new ClassPathResource("img/mylogo.gif")); 074 * message.addAttachment("myDocument.pdf", new ClassPathResource("doc/myDocument.pdf")); 075 * } 076 * });</pre> 077 * 078 * Consider using {@link MimeMailMessage} (which implements the common 079 * {@link org.springframework.mail.MailMessage} interface, just like 080 * {@link org.springframework.mail.SimpleMailMessage}) on top of this helper, 081 * in order to let message population code interact with a simple message 082 * or a MIME message through a common interface. 083 * 084 * <p><b>Warning regarding multipart mails:</b> Simple MIME messages that 085 * just contain HTML text but no inline elements or attachments will work on 086 * more or less any email client that is capable of HTML rendering. However, 087 * inline elements and attachments are still a major compatibility issue 088 * between email clients: It's virtually impossible to get inline elements 089 * and attachments working across Microsoft Outlook, Lotus Notes and Mac Mail. 090 * Consider choosing a specific multipart mode for your needs: The javadoc 091 * on the MULTIPART_MODE constants contains more detailed information. 092 * 093 * @author Juergen Hoeller 094 * @since 19.01.2004 095 * @see #setText(String, boolean) 096 * @see #setText(String, String) 097 * @see #addInline(String, org.springframework.core.io.Resource) 098 * @see #addAttachment(String, org.springframework.core.io.InputStreamSource) 099 * @see #MULTIPART_MODE_MIXED_RELATED 100 * @see #MULTIPART_MODE_RELATED 101 * @see #getMimeMessage() 102 * @see JavaMailSender 103 */ 104public class MimeMessageHelper { 105 106 /** 107 * Constant indicating a non-multipart message. 108 */ 109 public static final int MULTIPART_MODE_NO = 0; 110 111 /** 112 * Constant indicating a multipart message with a single root multipart 113 * element of type "mixed". Texts, inline elements and attachements 114 * will all get added to that root element. 115 * <p>This was Spring 1.0's default behavior. It is known to work properly 116 * on Outlook. However, other mail clients tend to misinterpret inline 117 * elements as attachments and/or show attachments inline as well. 118 */ 119 public static final int MULTIPART_MODE_MIXED = 1; 120 121 /** 122 * Constant indicating a multipart message with a single root multipart 123 * element of type "related". Texts, inline elements and attachements 124 * will all get added to that root element. 125 * <p>This was the default behavior from Spring 1.1 up to 1.2 final. 126 * This is the "Microsoft multipart mode", as natively sent by Outlook. 127 * It is known to work properly on Outlook, Outlook Express, Yahoo Mail, and 128 * to a large degree also on Mac Mail (with an additional attachment listed 129 * for an inline element, despite the inline element also shown inline). 130 * Does not work properly on Lotus Notes (attachments won't be shown there). 131 */ 132 public static final int MULTIPART_MODE_RELATED = 2; 133 134 /** 135 * Constant indicating a multipart message with a root multipart element 136 * "mixed" plus a nested multipart element of type "related". Texts and 137 * inline elements will get added to the nested "related" element, 138 * while attachments will get added to the "mixed" root element. 139 * <p>This is the default since Spring 1.2.1. This is arguably the most correct 140 * MIME structure, according to the MIME spec: It is known to work properly 141 * on Outlook, Outlook Express, Yahoo Mail, and Lotus Notes. Does not work 142 * properly on Mac Mail. If you target Mac Mail or experience issues with 143 * specific mails on Outlook, consider using MULTIPART_MODE_RELATED instead. 144 */ 145 public static final int MULTIPART_MODE_MIXED_RELATED = 3; 146 147 148 private static final String MULTIPART_SUBTYPE_MIXED = "mixed"; 149 150 private static final String MULTIPART_SUBTYPE_RELATED = "related"; 151 152 private static final String MULTIPART_SUBTYPE_ALTERNATIVE = "alternative"; 153 154 private static final String CONTENT_TYPE_ALTERNATIVE = "text/alternative"; 155 156 private static final String CONTENT_TYPE_HTML = "text/html"; 157 158 private static final String CONTENT_TYPE_CHARSET_SUFFIX = ";charset="; 159 160 private static final String HEADER_PRIORITY = "X-Priority"; 161 162 163 private final MimeMessage mimeMessage; 164 165 @Nullable 166 private MimeMultipart rootMimeMultipart; 167 168 @Nullable 169 private MimeMultipart mimeMultipart; 170 171 @Nullable 172 private final String encoding; 173 174 private FileTypeMap fileTypeMap; 175 176 private boolean encodeFilenames = true; 177 178 private boolean validateAddresses = false; 179 180 181 /** 182 * Create a new MimeMessageHelper for the given MimeMessage, 183 * assuming a simple text message (no multipart content, 184 * i.e. no alternative texts and no inline elements or attachments). 185 * <p>The character encoding for the message will be taken from 186 * the passed-in MimeMessage object, if carried there. Else, 187 * JavaMail's default encoding will be used. 188 * @param mimeMessage the mime message to work on 189 * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean) 190 * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) 191 * @see JavaMailSenderImpl#setDefaultEncoding 192 */ 193 public MimeMessageHelper(MimeMessage mimeMessage) { 194 this(mimeMessage, null); 195 } 196 197 /** 198 * Create a new MimeMessageHelper for the given MimeMessage, 199 * assuming a simple text message (no multipart content, 200 * i.e. no alternative texts and no inline elements or attachments). 201 * @param mimeMessage the mime message to work on 202 * @param encoding the character encoding to use for the message 203 * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean) 204 */ 205 public MimeMessageHelper(MimeMessage mimeMessage, @Nullable String encoding) { 206 this.mimeMessage = mimeMessage; 207 this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage)); 208 this.fileTypeMap = getDefaultFileTypeMap(mimeMessage); 209 } 210 211 /** 212 * Create a new MimeMessageHelper for the given MimeMessage, 213 * in multipart mode (supporting alternative texts, inline 214 * elements and attachments) if requested. 215 * <p>Consider using the MimeMessageHelper constructor that 216 * takes a multipartMode argument to choose a specific multipart 217 * mode other than MULTIPART_MODE_MIXED_RELATED. 218 * <p>The character encoding for the message will be taken from 219 * the passed-in MimeMessage object, if carried there. Else, 220 * JavaMail's default encoding will be used. 221 * @param mimeMessage the mime message to work on 222 * @param multipart whether to create a multipart message that 223 * supports alternative texts, inline elements and attachments 224 * (corresponds to MULTIPART_MODE_MIXED_RELATED) 225 * @throws MessagingException if multipart creation failed 226 * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int) 227 * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) 228 * @see JavaMailSenderImpl#setDefaultEncoding 229 */ 230 public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart) throws MessagingException { 231 this(mimeMessage, multipart, null); 232 } 233 234 /** 235 * Create a new MimeMessageHelper for the given MimeMessage, 236 * in multipart mode (supporting alternative texts, inline 237 * elements and attachments) if requested. 238 * <p>Consider using the MimeMessageHelper constructor that 239 * takes a multipartMode argument to choose a specific multipart 240 * mode other than MULTIPART_MODE_MIXED_RELATED. 241 * @param mimeMessage the mime message to work on 242 * @param multipart whether to create a multipart message that 243 * supports alternative texts, inline elements and attachments 244 * (corresponds to MULTIPART_MODE_MIXED_RELATED) 245 * @param encoding the character encoding to use for the message 246 * @throws MessagingException if multipart creation failed 247 * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int, String) 248 */ 249 public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart, @Nullable String encoding) 250 throws MessagingException { 251 252 this(mimeMessage, (multipart ? MULTIPART_MODE_MIXED_RELATED : MULTIPART_MODE_NO), encoding); 253 } 254 255 /** 256 * Create a new MimeMessageHelper for the given MimeMessage, 257 * in multipart mode (supporting alternative texts, inline 258 * elements and attachments) if requested. 259 * <p>The character encoding for the message will be taken from 260 * the passed-in MimeMessage object, if carried there. Else, 261 * JavaMail's default encoding will be used. 262 * @param mimeMessage the mime message to work on 263 * @param multipartMode which kind of multipart message to create 264 * (MIXED, RELATED, MIXED_RELATED, or NO) 265 * @throws MessagingException if multipart creation failed 266 * @see #MULTIPART_MODE_NO 267 * @see #MULTIPART_MODE_MIXED 268 * @see #MULTIPART_MODE_RELATED 269 * @see #MULTIPART_MODE_MIXED_RELATED 270 * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) 271 * @see JavaMailSenderImpl#setDefaultEncoding 272 */ 273 public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode) throws MessagingException { 274 this(mimeMessage, multipartMode, null); 275 } 276 277 /** 278 * Create a new MimeMessageHelper for the given MimeMessage, 279 * in multipart mode (supporting alternative texts, inline 280 * elements and attachments) if requested. 281 * @param mimeMessage the mime message to work on 282 * @param multipartMode which kind of multipart message to create 283 * (MIXED, RELATED, MIXED_RELATED, or NO) 284 * @param encoding the character encoding to use for the message 285 * @throws MessagingException if multipart creation failed 286 * @see #MULTIPART_MODE_NO 287 * @see #MULTIPART_MODE_MIXED 288 * @see #MULTIPART_MODE_RELATED 289 * @see #MULTIPART_MODE_MIXED_RELATED 290 */ 291 public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode, @Nullable String encoding) 292 throws MessagingException { 293 294 this.mimeMessage = mimeMessage; 295 createMimeMultiparts(mimeMessage, multipartMode); 296 this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage)); 297 this.fileTypeMap = getDefaultFileTypeMap(mimeMessage); 298 } 299 300 301 /** 302 * Return the underlying MimeMessage object. 303 */ 304 public final MimeMessage getMimeMessage() { 305 return this.mimeMessage; 306 } 307 308 309 /** 310 * Determine the MimeMultipart objects to use, which will be used 311 * to store attachments on the one hand and text(s) and inline elements 312 * on the other hand. 313 * <p>Texts and inline elements can either be stored in the root element 314 * itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED) or in a nested element 315 * rather than the root element directly (MULTIPART_MODE_MIXED_RELATED). 316 * <p>By default, the root MimeMultipart element will be of type "mixed" 317 * (MULTIPART_MODE_MIXED) or "related" (MULTIPART_MODE_RELATED). 318 * The main multipart element will either be added as nested element of 319 * type "related" (MULTIPART_MODE_MIXED_RELATED) or be identical to the root 320 * element itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED). 321 * @param mimeMessage the MimeMessage object to add the root MimeMultipart 322 * object to 323 * @param multipartMode the multipart mode, as passed into the constructor 324 * (MIXED, RELATED, MIXED_RELATED, or NO) 325 * @throws MessagingException if multipart creation failed 326 * @see #setMimeMultiparts 327 * @see #MULTIPART_MODE_NO 328 * @see #MULTIPART_MODE_MIXED 329 * @see #MULTIPART_MODE_RELATED 330 * @see #MULTIPART_MODE_MIXED_RELATED 331 */ 332 protected void createMimeMultiparts(MimeMessage mimeMessage, int multipartMode) throws MessagingException { 333 switch (multipartMode) { 334 case MULTIPART_MODE_NO: 335 setMimeMultiparts(null, null); 336 break; 337 case MULTIPART_MODE_MIXED: 338 MimeMultipart mixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED); 339 mimeMessage.setContent(mixedMultipart); 340 setMimeMultiparts(mixedMultipart, mixedMultipart); 341 break; 342 case MULTIPART_MODE_RELATED: 343 MimeMultipart relatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED); 344 mimeMessage.setContent(relatedMultipart); 345 setMimeMultiparts(relatedMultipart, relatedMultipart); 346 break; 347 case MULTIPART_MODE_MIXED_RELATED: 348 MimeMultipart rootMixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED); 349 mimeMessage.setContent(rootMixedMultipart); 350 MimeMultipart nestedRelatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED); 351 MimeBodyPart relatedBodyPart = new MimeBodyPart(); 352 relatedBodyPart.setContent(nestedRelatedMultipart); 353 rootMixedMultipart.addBodyPart(relatedBodyPart); 354 setMimeMultiparts(rootMixedMultipart, nestedRelatedMultipart); 355 break; 356 default: 357 throw new IllegalArgumentException("Only multipart modes MIXED_RELATED, RELATED and NO supported"); 358 } 359 } 360 361 /** 362 * Set the given MimeMultipart objects for use by this MimeMessageHelper. 363 * @param root the root MimeMultipart object, which attachments will be added to; 364 * or {@code null} to indicate no multipart at all 365 * @param main the main MimeMultipart object, which text(s) and inline elements 366 * will be added to (can be the same as the root multipart object, or an element 367 * nested underneath the root multipart element) 368 */ 369 protected final void setMimeMultiparts(@Nullable MimeMultipart root, @Nullable MimeMultipart main) { 370 this.rootMimeMultipart = root; 371 this.mimeMultipart = main; 372 } 373 374 /** 375 * Return whether this helper is in multipart mode, 376 * i.e. whether it holds a multipart message. 377 * @see #MimeMessageHelper(MimeMessage, boolean) 378 */ 379 public final boolean isMultipart() { 380 return (this.rootMimeMultipart != null); 381 } 382 383 /** 384 * Return the root MIME "multipart/mixed" object, if any. 385 * Can be used to manually add attachments. 386 * <p>This will be the direct content of the MimeMessage, 387 * in case of a multipart mail. 388 * @throws IllegalStateException if this helper is not in multipart mode 389 * @see #isMultipart 390 * @see #getMimeMessage 391 * @see javax.mail.internet.MimeMultipart#addBodyPart 392 */ 393 public final MimeMultipart getRootMimeMultipart() throws IllegalStateException { 394 if (this.rootMimeMultipart == null) { 395 throw new IllegalStateException("Not in multipart mode - " + 396 "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag " + 397 "if you need to set alternative texts or add inline elements or attachments."); 398 } 399 return this.rootMimeMultipart; 400 } 401 402 /** 403 * Return the underlying MIME "multipart/related" object, if any. 404 * Can be used to manually add body parts, inline elements, etc. 405 * <p>This will be nested within the root MimeMultipart, 406 * in case of a multipart mail. 407 * @throws IllegalStateException if this helper is not in multipart mode 408 * @see #isMultipart 409 * @see #getRootMimeMultipart 410 * @see javax.mail.internet.MimeMultipart#addBodyPart 411 */ 412 public final MimeMultipart getMimeMultipart() throws IllegalStateException { 413 if (this.mimeMultipart == null) { 414 throw new IllegalStateException("Not in multipart mode - " + 415 "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag " + 416 "if you need to set alternative texts or add inline elements or attachments."); 417 } 418 return this.mimeMultipart; 419 } 420 421 422 /** 423 * Determine the default encoding for the given MimeMessage. 424 * @param mimeMessage the passed-in MimeMessage 425 * @return the default encoding associated with the MimeMessage, 426 * or {@code null} if none found 427 */ 428 @Nullable 429 protected String getDefaultEncoding(MimeMessage mimeMessage) { 430 if (mimeMessage instanceof SmartMimeMessage) { 431 return ((SmartMimeMessage) mimeMessage).getDefaultEncoding(); 432 } 433 return null; 434 } 435 436 /** 437 * Return the specific character encoding used for this message, if any. 438 */ 439 @Nullable 440 public String getEncoding() { 441 return this.encoding; 442 } 443 444 /** 445 * Determine the default Java Activation FileTypeMap for the given MimeMessage. 446 * @param mimeMessage the passed-in MimeMessage 447 * @return the default FileTypeMap associated with the MimeMessage, 448 * or a default ConfigurableMimeFileTypeMap if none found for the message 449 * @see ConfigurableMimeFileTypeMap 450 */ 451 protected FileTypeMap getDefaultFileTypeMap(MimeMessage mimeMessage) { 452 if (mimeMessage instanceof SmartMimeMessage) { 453 FileTypeMap fileTypeMap = ((SmartMimeMessage) mimeMessage).getDefaultFileTypeMap(); 454 if (fileTypeMap != null) { 455 return fileTypeMap; 456 } 457 } 458 ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap(); 459 fileTypeMap.afterPropertiesSet(); 460 return fileTypeMap; 461 } 462 463 /** 464 * Set the Java Activation Framework {@code FileTypeMap} to use 465 * for determining the content type of inline content and attachments 466 * that get added to the message. 467 * <p>The default is the {@code FileTypeMap} that the underlying 468 * MimeMessage carries, if any, or the Activation Framework's default 469 * {@code FileTypeMap} instance else. 470 * @see #addInline 471 * @see #addAttachment 472 * @see #getDefaultFileTypeMap(javax.mail.internet.MimeMessage) 473 * @see JavaMailSenderImpl#setDefaultFileTypeMap 474 * @see javax.activation.FileTypeMap#getDefaultFileTypeMap 475 * @see ConfigurableMimeFileTypeMap 476 */ 477 public void setFileTypeMap(@Nullable FileTypeMap fileTypeMap) { 478 this.fileTypeMap = (fileTypeMap != null ? fileTypeMap : getDefaultFileTypeMap(getMimeMessage())); 479 } 480 481 /** 482 * Return the {@code FileTypeMap} used by this MimeMessageHelper. 483 * @see #setFileTypeMap 484 */ 485 public FileTypeMap getFileTypeMap() { 486 return this.fileTypeMap; 487 } 488 489 490 /** 491 * Set whether to encode attachment filenames passed to this helper's 492 * {@code #addAttachment} methods. 493 * <p>The default is {@code true} for compatibility with older email clients; 494 * turn this to {@code false} for standard MIME behavior. On a related note, 495 * check out JavaMail's {@code mail.mime.encodefilename} system property. 496 * @since 5.2.9 497 * @see #addAttachment(String, DataSource) 498 * @see MimeBodyPart#setFileName(String) 499 */ 500 public void setEncodeFilenames(boolean encodeFilenames) { 501 this.encodeFilenames = encodeFilenames; 502 } 503 504 /** 505 * Return whether to encode attachment filenames passed to this helper's 506 * {@code #addAttachment} methods. 507 * @since 5.2.9 508 * @see #setEncodeFilenames 509 */ 510 public boolean isEncodeFilenames() { 511 return this.encodeFilenames; 512 } 513 514 /** 515 * Set whether to validate all addresses which get passed to this helper. 516 * <p>The default is {@code false}. 517 * @see #validateAddress 518 */ 519 public void setValidateAddresses(boolean validateAddresses) { 520 this.validateAddresses = validateAddresses; 521 } 522 523 /** 524 * Return whether this helper will validate all addresses passed to it. 525 * @see #setValidateAddresses 526 */ 527 public boolean isValidateAddresses() { 528 return this.validateAddresses; 529 } 530 531 /** 532 * Validate the given mail address. 533 * Called by all of MimeMessageHelper's address setters and adders. 534 * <p>The default implementation invokes {@link InternetAddress#validate()}, 535 * provided that address validation is activated for the helper instance. 536 * @param address the address to validate 537 * @throws AddressException if validation failed 538 * @see #isValidateAddresses() 539 * @see javax.mail.internet.InternetAddress#validate() 540 */ 541 protected void validateAddress(InternetAddress address) throws AddressException { 542 if (isValidateAddresses()) { 543 address.validate(); 544 } 545 } 546 547 /** 548 * Validate all given mail addresses. 549 * <p>The default implementation simply delegates to {@link #validateAddress} 550 * for each address. 551 * @param addresses the addresses to validate 552 * @throws AddressException if validation failed 553 * @see #validateAddress(InternetAddress) 554 */ 555 protected void validateAddresses(InternetAddress[] addresses) throws AddressException { 556 for (InternetAddress address : addresses) { 557 validateAddress(address); 558 } 559 } 560 561 562 public void setFrom(InternetAddress from) throws MessagingException { 563 Assert.notNull(from, "From address must not be null"); 564 validateAddress(from); 565 this.mimeMessage.setFrom(from); 566 } 567 568 public void setFrom(String from) throws MessagingException { 569 Assert.notNull(from, "From address must not be null"); 570 setFrom(parseAddress(from)); 571 } 572 573 public void setFrom(String from, String personal) throws MessagingException, UnsupportedEncodingException { 574 Assert.notNull(from, "From address must not be null"); 575 setFrom(getEncoding() != null ? 576 new InternetAddress(from, personal, getEncoding()) : new InternetAddress(from, personal)); 577 } 578 579 public void setReplyTo(InternetAddress replyTo) throws MessagingException { 580 Assert.notNull(replyTo, "Reply-to address must not be null"); 581 validateAddress(replyTo); 582 this.mimeMessage.setReplyTo(new InternetAddress[] {replyTo}); 583 } 584 585 public void setReplyTo(String replyTo) throws MessagingException { 586 Assert.notNull(replyTo, "Reply-to address must not be null"); 587 setReplyTo(parseAddress(replyTo)); 588 } 589 590 public void setReplyTo(String replyTo, String personal) throws MessagingException, UnsupportedEncodingException { 591 Assert.notNull(replyTo, "Reply-to address must not be null"); 592 InternetAddress replyToAddress = (getEncoding() != null) ? 593 new InternetAddress(replyTo, personal, getEncoding()) : new InternetAddress(replyTo, personal); 594 setReplyTo(replyToAddress); 595 } 596 597 598 public void setTo(InternetAddress to) throws MessagingException { 599 Assert.notNull(to, "To address must not be null"); 600 validateAddress(to); 601 this.mimeMessage.setRecipient(Message.RecipientType.TO, to); 602 } 603 604 public void setTo(InternetAddress[] to) throws MessagingException { 605 Assert.notNull(to, "To address array must not be null"); 606 validateAddresses(to); 607 this.mimeMessage.setRecipients(Message.RecipientType.TO, to); 608 } 609 610 public void setTo(String to) throws MessagingException { 611 Assert.notNull(to, "To address must not be null"); 612 setTo(parseAddress(to)); 613 } 614 615 public void setTo(String[] to) throws MessagingException { 616 Assert.notNull(to, "To address array must not be null"); 617 InternetAddress[] addresses = new InternetAddress[to.length]; 618 for (int i = 0; i < to.length; i++) { 619 addresses[i] = parseAddress(to[i]); 620 } 621 setTo(addresses); 622 } 623 624 public void addTo(InternetAddress to) throws MessagingException { 625 Assert.notNull(to, "To address must not be null"); 626 validateAddress(to); 627 this.mimeMessage.addRecipient(Message.RecipientType.TO, to); 628 } 629 630 public void addTo(String to) throws MessagingException { 631 Assert.notNull(to, "To address must not be null"); 632 addTo(parseAddress(to)); 633 } 634 635 public void addTo(String to, String personal) throws MessagingException, UnsupportedEncodingException { 636 Assert.notNull(to, "To address must not be null"); 637 addTo(getEncoding() != null ? 638 new InternetAddress(to, personal, getEncoding()) : 639 new InternetAddress(to, personal)); 640 } 641 642 643 public void setCc(InternetAddress cc) throws MessagingException { 644 Assert.notNull(cc, "Cc address must not be null"); 645 validateAddress(cc); 646 this.mimeMessage.setRecipient(Message.RecipientType.CC, cc); 647 } 648 649 public void setCc(InternetAddress[] cc) throws MessagingException { 650 Assert.notNull(cc, "Cc address array must not be null"); 651 validateAddresses(cc); 652 this.mimeMessage.setRecipients(Message.RecipientType.CC, cc); 653 } 654 655 public void setCc(String cc) throws MessagingException { 656 Assert.notNull(cc, "Cc address must not be null"); 657 setCc(parseAddress(cc)); 658 } 659 660 public void setCc(String[] cc) throws MessagingException { 661 Assert.notNull(cc, "Cc address array must not be null"); 662 InternetAddress[] addresses = new InternetAddress[cc.length]; 663 for (int i = 0; i < cc.length; i++) { 664 addresses[i] = parseAddress(cc[i]); 665 } 666 setCc(addresses); 667 } 668 669 public void addCc(InternetAddress cc) throws MessagingException { 670 Assert.notNull(cc, "Cc address must not be null"); 671 validateAddress(cc); 672 this.mimeMessage.addRecipient(Message.RecipientType.CC, cc); 673 } 674 675 public void addCc(String cc) throws MessagingException { 676 Assert.notNull(cc, "Cc address must not be null"); 677 addCc(parseAddress(cc)); 678 } 679 680 public void addCc(String cc, String personal) throws MessagingException, UnsupportedEncodingException { 681 Assert.notNull(cc, "Cc address must not be null"); 682 addCc(getEncoding() != null ? 683 new InternetAddress(cc, personal, getEncoding()) : 684 new InternetAddress(cc, personal)); 685 } 686 687 688 public void setBcc(InternetAddress bcc) throws MessagingException { 689 Assert.notNull(bcc, "Bcc address must not be null"); 690 validateAddress(bcc); 691 this.mimeMessage.setRecipient(Message.RecipientType.BCC, bcc); 692 } 693 694 public void setBcc(InternetAddress[] bcc) throws MessagingException { 695 Assert.notNull(bcc, "Bcc address array must not be null"); 696 validateAddresses(bcc); 697 this.mimeMessage.setRecipients(Message.RecipientType.BCC, bcc); 698 } 699 700 public void setBcc(String bcc) throws MessagingException { 701 Assert.notNull(bcc, "Bcc address must not be null"); 702 setBcc(parseAddress(bcc)); 703 } 704 705 public void setBcc(String[] bcc) throws MessagingException { 706 Assert.notNull(bcc, "Bcc address array must not be null"); 707 InternetAddress[] addresses = new InternetAddress[bcc.length]; 708 for (int i = 0; i < bcc.length; i++) { 709 addresses[i] = parseAddress(bcc[i]); 710 } 711 setBcc(addresses); 712 } 713 714 public void addBcc(InternetAddress bcc) throws MessagingException { 715 Assert.notNull(bcc, "Bcc address must not be null"); 716 validateAddress(bcc); 717 this.mimeMessage.addRecipient(Message.RecipientType.BCC, bcc); 718 } 719 720 public void addBcc(String bcc) throws MessagingException { 721 Assert.notNull(bcc, "Bcc address must not be null"); 722 addBcc(parseAddress(bcc)); 723 } 724 725 public void addBcc(String bcc, String personal) throws MessagingException, UnsupportedEncodingException { 726 Assert.notNull(bcc, "Bcc address must not be null"); 727 addBcc(getEncoding() != null ? 728 new InternetAddress(bcc, personal, getEncoding()) : 729 new InternetAddress(bcc, personal)); 730 } 731 732 private InternetAddress parseAddress(String address) throws MessagingException { 733 InternetAddress[] parsed = InternetAddress.parse(address); 734 if (parsed.length != 1) { 735 throw new AddressException("Illegal address", address); 736 } 737 InternetAddress raw = parsed[0]; 738 try { 739 return (getEncoding() != null ? 740 new InternetAddress(raw.getAddress(), raw.getPersonal(), getEncoding()) : raw); 741 } 742 catch (UnsupportedEncodingException ex) { 743 throw new MessagingException("Failed to parse embedded personal name to correct encoding", ex); 744 } 745 } 746 747 748 /** 749 * Set the priority ("X-Priority" header) of the message. 750 * @param priority the priority value; 751 * typically between 1 (highest) and 5 (lowest) 752 * @throws MessagingException in case of errors 753 */ 754 public void setPriority(int priority) throws MessagingException { 755 this.mimeMessage.setHeader(HEADER_PRIORITY, Integer.toString(priority)); 756 } 757 758 /** 759 * Set the sent-date of the message. 760 * @param sentDate the date to set (never {@code null}) 761 * @throws MessagingException in case of errors 762 */ 763 public void setSentDate(Date sentDate) throws MessagingException { 764 Assert.notNull(sentDate, "Sent date must not be null"); 765 this.mimeMessage.setSentDate(sentDate); 766 } 767 768 /** 769 * Set the subject of the message, using the correct encoding. 770 * @param subject the subject text 771 * @throws MessagingException in case of errors 772 */ 773 public void setSubject(String subject) throws MessagingException { 774 Assert.notNull(subject, "Subject must not be null"); 775 if (getEncoding() != null) { 776 this.mimeMessage.setSubject(subject, getEncoding()); 777 } 778 else { 779 this.mimeMessage.setSubject(subject); 780 } 781 } 782 783 784 /** 785 * Set the given text directly as content in non-multipart mode 786 * or as default body part in multipart mode. 787 * Always applies the default content type "text/plain". 788 * <p><b>NOTE:</b> Invoke {@link #addInline} <i>after</i> {@code setText}; 789 * else, mail readers might not be able to resolve inline references correctly. 790 * @param text the text for the message 791 * @throws MessagingException in case of errors 792 */ 793 public void setText(String text) throws MessagingException { 794 setText(text, false); 795 } 796 797 /** 798 * Set the given text directly as content in non-multipart mode 799 * or as default body part in multipart mode. 800 * The "html" flag determines the content type to apply. 801 * <p><b>NOTE:</b> Invoke {@link #addInline} <i>after</i> {@code setText}; 802 * else, mail readers might not be able to resolve inline references correctly. 803 * @param text the text for the message 804 * @param html whether to apply content type "text/html" for an 805 * HTML mail, using default content type ("text/plain") else 806 * @throws MessagingException in case of errors 807 */ 808 public void setText(String text, boolean html) throws MessagingException { 809 Assert.notNull(text, "Text must not be null"); 810 MimePart partToUse; 811 if (isMultipart()) { 812 partToUse = getMainPart(); 813 } 814 else { 815 partToUse = this.mimeMessage; 816 } 817 if (html) { 818 setHtmlTextToMimePart(partToUse, text); 819 } 820 else { 821 setPlainTextToMimePart(partToUse, text); 822 } 823 } 824 825 /** 826 * Set the given plain text and HTML text as alternatives, offering 827 * both options to the email client. Requires multipart mode. 828 * <p><b>NOTE:</b> Invoke {@link #addInline} <i>after</i> {@code setText}; 829 * else, mail readers might not be able to resolve inline references correctly. 830 * @param plainText the plain text for the message 831 * @param htmlText the HTML text for the message 832 * @throws MessagingException in case of errors 833 */ 834 public void setText(String plainText, String htmlText) throws MessagingException { 835 Assert.notNull(plainText, "Plain text must not be null"); 836 Assert.notNull(htmlText, "HTML text must not be null"); 837 838 MimeMultipart messageBody = new MimeMultipart(MULTIPART_SUBTYPE_ALTERNATIVE); 839 getMainPart().setContent(messageBody, CONTENT_TYPE_ALTERNATIVE); 840 841 // Create the plain text part of the message. 842 MimeBodyPart plainTextPart = new MimeBodyPart(); 843 setPlainTextToMimePart(plainTextPart, plainText); 844 messageBody.addBodyPart(plainTextPart); 845 846 // Create the HTML text part of the message. 847 MimeBodyPart htmlTextPart = new MimeBodyPart(); 848 setHtmlTextToMimePart(htmlTextPart, htmlText); 849 messageBody.addBodyPart(htmlTextPart); 850 } 851 852 private MimeBodyPart getMainPart() throws MessagingException { 853 MimeMultipart mimeMultipart = getMimeMultipart(); 854 MimeBodyPart bodyPart = null; 855 for (int i = 0; i < mimeMultipart.getCount(); i++) { 856 BodyPart bp = mimeMultipart.getBodyPart(i); 857 if (bp.getFileName() == null) { 858 bodyPart = (MimeBodyPart) bp; 859 } 860 } 861 if (bodyPart == null) { 862 MimeBodyPart mimeBodyPart = new MimeBodyPart(); 863 mimeMultipart.addBodyPart(mimeBodyPart); 864 bodyPart = mimeBodyPart; 865 } 866 return bodyPart; 867 } 868 869 private void setPlainTextToMimePart(MimePart mimePart, String text) throws MessagingException { 870 if (getEncoding() != null) { 871 mimePart.setText(text, getEncoding()); 872 } 873 else { 874 mimePart.setText(text); 875 } 876 } 877 878 private void setHtmlTextToMimePart(MimePart mimePart, String text) throws MessagingException { 879 if (getEncoding() != null) { 880 mimePart.setContent(text, CONTENT_TYPE_HTML + CONTENT_TYPE_CHARSET_SUFFIX + getEncoding()); 881 } 882 else { 883 mimePart.setContent(text, CONTENT_TYPE_HTML); 884 } 885 } 886 887 888 /** 889 * Add an inline element to the MimeMessage, taking the content from a 890 * {@code javax.activation.DataSource}. 891 * <p>Note that the InputStream returned by the DataSource implementation 892 * needs to be a <i>fresh one on each call</i>, as JavaMail will invoke 893 * {@code getInputStream()} multiple times. 894 * <p><b>NOTE:</b> Invoke {@code addInline} <i>after</i> {@link #setText}; 895 * else, mail readers might not be able to resolve inline references correctly. 896 * @param contentId the content ID to use. Will end up as "Content-ID" header 897 * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". 898 * Can be referenced in HTML source via src="cid:myId" expressions. 899 * @param dataSource the {@code javax.activation.DataSource} to take 900 * the content from, determining the InputStream and the content type 901 * @throws MessagingException in case of errors 902 * @see #addInline(String, java.io.File) 903 * @see #addInline(String, org.springframework.core.io.Resource) 904 */ 905 public void addInline(String contentId, DataSource dataSource) throws MessagingException { 906 Assert.notNull(contentId, "Content ID must not be null"); 907 Assert.notNull(dataSource, "DataSource must not be null"); 908 MimeBodyPart mimeBodyPart = new MimeBodyPart(); 909 mimeBodyPart.setDisposition(MimeBodyPart.INLINE); 910 mimeBodyPart.setContentID("<" + contentId + ">"); 911 mimeBodyPart.setDataHandler(new DataHandler(dataSource)); 912 getMimeMultipart().addBodyPart(mimeBodyPart); 913 } 914 915 /** 916 * Add an inline element to the MimeMessage, taking the content from a 917 * {@code java.io.File}. 918 * <p>The content type will be determined by the name of the given 919 * content file. Do not use this for temporary files with arbitrary 920 * filenames (possibly ending in ".tmp" or the like)! 921 * <p><b>NOTE:</b> Invoke {@code addInline} <i>after</i> {@link #setText}; 922 * else, mail readers might not be able to resolve inline references correctly. 923 * @param contentId the content ID to use. Will end up as "Content-ID" header 924 * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". 925 * Can be referenced in HTML source via src="cid:myId" expressions. 926 * @param file the File resource to take the content from 927 * @throws MessagingException in case of errors 928 * @see #setText 929 * @see #addInline(String, org.springframework.core.io.Resource) 930 * @see #addInline(String, javax.activation.DataSource) 931 */ 932 public void addInline(String contentId, File file) throws MessagingException { 933 Assert.notNull(file, "File must not be null"); 934 FileDataSource dataSource = new FileDataSource(file); 935 dataSource.setFileTypeMap(getFileTypeMap()); 936 addInline(contentId, dataSource); 937 } 938 939 /** 940 * Add an inline element to the MimeMessage, taking the content from a 941 * {@code org.springframework.core.io.Resource}. 942 * <p>The content type will be determined by the name of the given 943 * content file. Do not use this for temporary files with arbitrary 944 * filenames (possibly ending in ".tmp" or the like)! 945 * <p>Note that the InputStream returned by the Resource implementation 946 * needs to be a <i>fresh one on each call</i>, as JavaMail will invoke 947 * {@code getInputStream()} multiple times. 948 * <p><b>NOTE:</b> Invoke {@code addInline} <i>after</i> {@link #setText}; 949 * else, mail readers might not be able to resolve inline references correctly. 950 * @param contentId the content ID to use. Will end up as "Content-ID" header 951 * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". 952 * Can be referenced in HTML source via src="cid:myId" expressions. 953 * @param resource the resource to take the content from 954 * @throws MessagingException in case of errors 955 * @see #setText 956 * @see #addInline(String, java.io.File) 957 * @see #addInline(String, javax.activation.DataSource) 958 */ 959 public void addInline(String contentId, Resource resource) throws MessagingException { 960 Assert.notNull(resource, "Resource must not be null"); 961 String contentType = getFileTypeMap().getContentType(resource.getFilename()); 962 addInline(contentId, resource, contentType); 963 } 964 965 /** 966 * Add an inline element to the MimeMessage, taking the content from an 967 * {@code org.springframework.core.InputStreamResource}, and 968 * specifying the content type explicitly. 969 * <p>You can determine the content type for any given filename via a Java 970 * Activation Framework's FileTypeMap, for example the one held by this helper. 971 * <p>Note that the InputStream returned by the InputStreamSource implementation 972 * needs to be a <i>fresh one on each call</i>, as JavaMail will invoke 973 * {@code getInputStream()} multiple times. 974 * <p><b>NOTE:</b> Invoke {@code addInline} <i>after</i> {@code setText}; 975 * else, mail readers might not be able to resolve inline references correctly. 976 * @param contentId the content ID to use. Will end up as "Content-ID" header 977 * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". 978 * Can be referenced in HTML source via src="cid:myId" expressions. 979 * @param inputStreamSource the resource to take the content from 980 * @param contentType the content type to use for the element 981 * @throws MessagingException in case of errors 982 * @see #setText 983 * @see #getFileTypeMap 984 * @see #addInline(String, org.springframework.core.io.Resource) 985 * @see #addInline(String, javax.activation.DataSource) 986 */ 987 public void addInline(String contentId, InputStreamSource inputStreamSource, String contentType) 988 throws MessagingException { 989 990 Assert.notNull(inputStreamSource, "InputStreamSource must not be null"); 991 if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) { 992 throw new IllegalArgumentException( 993 "Passed-in Resource contains an open stream: invalid argument. " + 994 "JavaMail requires an InputStreamSource that creates a fresh stream for every call."); 995 } 996 DataSource dataSource = createDataSource(inputStreamSource, contentType, "inline"); 997 addInline(contentId, dataSource); 998 } 999 1000 /** 1001 * Add an attachment to the MimeMessage, taking the content from a 1002 * {@code javax.activation.DataSource}. 1003 * <p>Note that the InputStream returned by the DataSource implementation 1004 * needs to be a <i>fresh one on each call</i>, as JavaMail will invoke 1005 * {@code getInputStream()} multiple times. 1006 * @param attachmentFilename the name of the attachment as it will 1007 * appear in the mail (the content type will be determined by this) 1008 * @param dataSource the {@code javax.activation.DataSource} to take 1009 * the content from, determining the InputStream and the content type 1010 * @throws MessagingException in case of errors 1011 * @see #addAttachment(String, org.springframework.core.io.InputStreamSource) 1012 * @see #addAttachment(String, java.io.File) 1013 */ 1014 public void addAttachment(String attachmentFilename, DataSource dataSource) throws MessagingException { 1015 Assert.notNull(attachmentFilename, "Attachment filename must not be null"); 1016 Assert.notNull(dataSource, "DataSource must not be null"); 1017 try { 1018 MimeBodyPart mimeBodyPart = new MimeBodyPart(); 1019 mimeBodyPart.setDisposition(MimeBodyPart.ATTACHMENT); 1020 mimeBodyPart.setFileName(isEncodeFilenames() ? 1021 MimeUtility.encodeText(attachmentFilename) : attachmentFilename); 1022 mimeBodyPart.setDataHandler(new DataHandler(dataSource)); 1023 getRootMimeMultipart().addBodyPart(mimeBodyPart); 1024 } 1025 catch (UnsupportedEncodingException ex) { 1026 throw new MessagingException("Failed to encode attachment filename", ex); 1027 } 1028 } 1029 1030 /** 1031 * Add an attachment to the MimeMessage, taking the content from a 1032 * {@code java.io.File}. 1033 * <p>The content type will be determined by the name of the given 1034 * content file. Do not use this for temporary files with arbitrary 1035 * filenames (possibly ending in ".tmp" or the like)! 1036 * @param attachmentFilename the name of the attachment as it will 1037 * appear in the mail 1038 * @param file the File resource to take the content from 1039 * @throws MessagingException in case of errors 1040 * @see #addAttachment(String, org.springframework.core.io.InputStreamSource) 1041 * @see #addAttachment(String, javax.activation.DataSource) 1042 */ 1043 public void addAttachment(String attachmentFilename, File file) throws MessagingException { 1044 Assert.notNull(file, "File must not be null"); 1045 FileDataSource dataSource = new FileDataSource(file); 1046 dataSource.setFileTypeMap(getFileTypeMap()); 1047 addAttachment(attachmentFilename, dataSource); 1048 } 1049 1050 /** 1051 * Add an attachment to the MimeMessage, taking the content from an 1052 * {@code org.springframework.core.io.InputStreamResource}. 1053 * <p>The content type will be determined by the given filename for 1054 * the attachment. Thus, any content source will be fine, including 1055 * temporary files with arbitrary filenames. 1056 * <p>Note that the InputStream returned by the InputStreamSource 1057 * implementation needs to be a <i>fresh one on each call</i>, as 1058 * JavaMail will invoke {@code getInputStream()} multiple times. 1059 * @param attachmentFilename the name of the attachment as it will 1060 * appear in the mail 1061 * @param inputStreamSource the resource to take the content from 1062 * (all of Spring's Resource implementations can be passed in here) 1063 * @throws MessagingException in case of errors 1064 * @see #addAttachment(String, java.io.File) 1065 * @see #addAttachment(String, javax.activation.DataSource) 1066 * @see org.springframework.core.io.Resource 1067 */ 1068 public void addAttachment(String attachmentFilename, InputStreamSource inputStreamSource) 1069 throws MessagingException { 1070 1071 String contentType = getFileTypeMap().getContentType(attachmentFilename); 1072 addAttachment(attachmentFilename, inputStreamSource, contentType); 1073 } 1074 1075 /** 1076 * Add an attachment to the MimeMessage, taking the content from an 1077 * {@code org.springframework.core.io.InputStreamResource}. 1078 * <p>Note that the InputStream returned by the InputStreamSource 1079 * implementation needs to be a <i>fresh one on each call</i>, as 1080 * JavaMail will invoke {@code getInputStream()} multiple times. 1081 * @param attachmentFilename the name of the attachment as it will 1082 * appear in the mail 1083 * @param inputStreamSource the resource to take the content from 1084 * (all of Spring's Resource implementations can be passed in here) 1085 * @param contentType the content type to use for the element 1086 * @throws MessagingException in case of errors 1087 * @see #addAttachment(String, java.io.File) 1088 * @see #addAttachment(String, javax.activation.DataSource) 1089 * @see org.springframework.core.io.Resource 1090 */ 1091 public void addAttachment( 1092 String attachmentFilename, InputStreamSource inputStreamSource, String contentType) 1093 throws MessagingException { 1094 1095 Assert.notNull(inputStreamSource, "InputStreamSource must not be null"); 1096 if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) { 1097 throw new IllegalArgumentException( 1098 "Passed-in Resource contains an open stream: invalid argument. " + 1099 "JavaMail requires an InputStreamSource that creates a fresh stream for every call."); 1100 } 1101 DataSource dataSource = createDataSource(inputStreamSource, contentType, attachmentFilename); 1102 addAttachment(attachmentFilename, dataSource); 1103 } 1104 1105 /** 1106 * Create an Activation Framework DataSource for the given InputStreamSource. 1107 * @param inputStreamSource the InputStreamSource (typically a Spring Resource) 1108 * @param contentType the content type 1109 * @param name the name of the DataSource 1110 * @return the Activation Framework DataSource 1111 */ 1112 protected DataSource createDataSource( 1113 final InputStreamSource inputStreamSource, final String contentType, final String name) { 1114 1115 return new DataSource() { 1116 @Override 1117 public InputStream getInputStream() throws IOException { 1118 return inputStreamSource.getInputStream(); 1119 } 1120 @Override 1121 public OutputStream getOutputStream() { 1122 throw new UnsupportedOperationException("Read-only javax.activation.DataSource"); 1123 } 1124 @Override 1125 public String getContentType() { 1126 return contentType; 1127 } 1128 @Override 1129 public String getName() { 1130 return name; 1131 } 1132 }; 1133 } 1134 1135}