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