com.wl4g.infra.common.notification.email.internal.MimeMessageHelper Maven / Gradle / Ivy
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wl4g.infra.common.notification.email.internal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.activation.FileTypeMap;
import javax.annotation.Nullable;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimePart;
import javax.mail.internet.MimeUtility;
import com.wl4g.infra.common.lang.Assert2;
import com.wl4g.infra.common.notification.email.internal.JavaMailSender.MimeMessagePreparator;
import com.wl4g.infra.common.resource.StreamResource;
/**
* Helper class for populating a {@link javax.mail.internet.MimeMessage}.
*
*
* Mirrors the simple setters of
* {@link com.wl4g.infra.common.notification.email.internal.springframework.mail.SimpleMailMessage}, directly applying the
* values to the underlying MimeMessage. Allows for defining a character
* encoding for the entire message, automatically applied by all methods of this
* helper class.
*
*
* Offers support for HTML text content, inline elements such as images, and
* typical mail attachments. Also supports personal names that accompany mail
* addresses. Note that advanced settings can still be applied directly to the
* underlying MimeMessage object!
*
*
* Typically used in {@link MimeMessagePreparator} implementations or
* {@link JavaMailSender} client code: simply instantiating it as a MimeMessage
* wrapper, invoking setters on the wrapper, using the underlying MimeMessage
* for mail sending. Also used internally by {@link JavaMailSenderImpl}.
*
*
* Sample code for an HTML mail with an inline image and a PDF attachment:
*
*
* mailSender.send(new MimeMessagePreparator() {
* public void prepare(MimeMessage mimeMessage) throws MessagingException {
* MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
* message.setFrom("[email protected]");
* message.setTo("[email protected]");
* message.setSubject("my subject");
* message.setText("my text <img src='cid:myLogo'>", true);
* message.addInline("myLogo", new ClassPathResource("img/mylogo.gif"));
* message.addAttachment("myDocument.pdf", new ClassPathResource("doc/myDocument.pdf"));
* }
* });
*
*
* Consider using {@link MimeMailMessage} (which implements the common
* {@link com.wl4g.infra.common.notification.email.internal.springframework.mail.MailMessage} interface, just like
* {@link com.wl4g.infra.common.notification.email.internal.springframework.mail.SimpleMailMessage}) on top of this helper, in
* order to let message population code interact with a simple message or a MIME
* message through a common interface.
*
*
* Warning regarding multipart mails: Simple MIME messages that just
* contain HTML text but no inline elements or attachments will work on more or
* less any email client that is capable of HTML rendering. However, inline
* elements and attachments are still a major compatibility issue between email
* clients: It's virtually impossible to get inline elements and attachments
* working across Microsoft Outlook, Lotus Notes and Mac Mail. Consider choosing
* a specific multipart mode for your needs: The javadoc on the MULTIPART_MODE
* constants contains more detailed information.
*
* @author Juergen Hoeller
* @since 19.01.2004
* @see #setText(String, boolean)
* @see #setText(String, String)
* @see #addInline(String, org.springframework.core.io.Resource)
* @see #addAttachment(String, org.springframework.core.io.InputStreamSource)
* @see #MULTIPART_MODE_MIXED_RELATED
* @see #MULTIPART_MODE_RELATED
* @see #getMimeMessage()
* @see JavaMailSender
*/
public class MimeMessageHelper {
/**
* Constant indicating a non-multipart message.
*/
public static final int MULTIPART_MODE_NO = 0;
/**
* Constant indicating a multipart message with a single root multipart
* element of type "mixed". Texts, inline elements and attachements will all
* get added to that root element.
*
* This was Spring 1.0's default behavior. It is known to work properly on
* Outlook. However, other mail clients tend to misinterpret inline elements
* as attachments and/or show attachments inline as well.
*/
public static final int MULTIPART_MODE_MIXED = 1;
/**
* Constant indicating a multipart message with a single root multipart
* element of type "related". Texts, inline elements and attachements will
* all get added to that root element.
*
* This was the default behavior from Spring 1.1 up to 1.2 final. This is
* the "Microsoft multipart mode", as natively sent by Outlook. It is known
* to work properly on Outlook, Outlook Express, Yahoo Mail, and to a large
* degree also on Mac Mail (with an additional attachment listed for an
* inline element, despite the inline element also shown inline). Does not
* work properly on Lotus Notes (attachments won't be shown there).
*/
public static final int MULTIPART_MODE_RELATED = 2;
/**
* Constant indicating a multipart message with a root multipart element
* "mixed" plus a nested multipart element of type "related". Texts and
* inline elements will get added to the nested "related" element, while
* attachments will get added to the "mixed" root element.
*
* This is the default since Spring 1.2.1. This is arguably the most correct
* MIME structure, according to the MIME spec: It is known to work properly
* on Outlook, Outlook Express, Yahoo Mail, and Lotus Notes. Does not work
* properly on Mac Mail. If you target Mac Mail or experience issues with
* specific mails on Outlook, consider using MULTIPART_MODE_RELATED instead.
*/
public static final int MULTIPART_MODE_MIXED_RELATED = 3;
private static final String MULTIPART_SUBTYPE_MIXED = "mixed";
private static final String MULTIPART_SUBTYPE_RELATED = "related";
private static final String MULTIPART_SUBTYPE_ALTERNATIVE = "alternative";
private static final String CONTENT_TYPE_ALTERNATIVE = "text/alternative";
private static final String CONTENT_TYPE_HTML = "text/html";
private static final String CONTENT_TYPE_CHARSET_SUFFIX = ";charset=";
private static final String HEADER_PRIORITY = "X-Priority";
private final MimeMessage mimeMessage;
@Nullable
private MimeMultipart rootMimeMultipart;
@Nullable
private MimeMultipart mimeMultipart;
@Nullable
private final String encoding;
private FileTypeMap fileTypeMap;
private boolean encodeFilenames = false;
private boolean validateAddresses = false;
/**
* Create a new MimeMessageHelper for the given MimeMessage, assuming a
* simple text message (no multipart content, i.e. no alternative texts and
* no inline elements or attachments).
*
* The character encoding for the message will be taken from the passed-in
* MimeMessage object, if carried there. Else, JavaMail's default encoding
* will be used.
*
* @param mimeMessage
* the mime message to work on
* @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean)
* @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
* @see JavaMailSenderImpl#setDefaultEncoding
*/
public MimeMessageHelper(MimeMessage mimeMessage) {
this(mimeMessage, null);
}
/**
* Create a new MimeMessageHelper for the given MimeMessage, assuming a
* simple text message (no multipart content, i.e. no alternative texts and
* no inline elements or attachments).
*
* @param mimeMessage
* the mime message to work on
* @param encoding
* the character encoding to use for the message
* @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean)
*/
public MimeMessageHelper(MimeMessage mimeMessage, @Nullable String encoding) {
this.mimeMessage = mimeMessage;
this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage));
this.fileTypeMap = getDefaultFileTypeMap(mimeMessage);
}
/**
* Create a new MimeMessageHelper for the given MimeMessage, in multipart
* mode (supporting alternative texts, inline elements and attachments) if
* requested.
*
* Consider using the MimeMessageHelper constructor that takes a
* multipartMode argument to choose a specific multipart mode other than
* MULTIPART_MODE_MIXED_RELATED.
*
* The character encoding for the message will be taken from the passed-in
* MimeMessage object, if carried there. Else, JavaMail's default encoding
* will be used.
*
* @param mimeMessage
* the mime message to work on
* @param multipart
* whether to create a multipart message that supports
* alternative texts, inline elements and attachments
* (corresponds to MULTIPART_MODE_MIXED_RELATED)
* @throws MessagingException
* if multipart creation failed
* @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int)
* @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
* @see JavaMailSenderImpl#setDefaultEncoding
*/
public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart) throws MessagingException {
this(mimeMessage, multipart, null);
}
/**
* Create a new MimeMessageHelper for the given MimeMessage, in multipart
* mode (supporting alternative texts, inline elements and attachments) if
* requested.
*
* Consider using the MimeMessageHelper constructor that takes a
* multipartMode argument to choose a specific multipart mode other than
* MULTIPART_MODE_MIXED_RELATED.
*
* @param mimeMessage
* the mime message to work on
* @param multipart
* whether to create a multipart message that supports
* alternative texts, inline elements and attachments
* (corresponds to MULTIPART_MODE_MIXED_RELATED)
* @param encoding
* the character encoding to use for the message
* @throws MessagingException
* if multipart creation failed
* @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int, String)
*/
public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart, @Nullable String encoding) throws MessagingException {
this(mimeMessage, (multipart ? MULTIPART_MODE_MIXED_RELATED : MULTIPART_MODE_NO), encoding);
}
/**
* Create a new MimeMessageHelper for the given MimeMessage, in multipart
* mode (supporting alternative texts, inline elements and attachments) if
* requested.
*
* The character encoding for the message will be taken from the passed-in
* MimeMessage object, if carried there. Else, JavaMail's default encoding
* will be used.
*
* @param mimeMessage
* the mime message to work on
* @param multipartMode
* which kind of multipart message to create (MIXED, RELATED,
* MIXED_RELATED, or NO)
* @throws MessagingException
* if multipart creation failed
* @see #MULTIPART_MODE_NO
* @see #MULTIPART_MODE_MIXED
* @see #MULTIPART_MODE_RELATED
* @see #MULTIPART_MODE_MIXED_RELATED
* @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
* @see JavaMailSenderImpl#setDefaultEncoding
*/
public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode) throws MessagingException {
this(mimeMessage, multipartMode, null);
}
/**
* Create a new MimeMessageHelper for the given MimeMessage, in multipart
* mode (supporting alternative texts, inline elements and attachments) if
* requested.
*
* @param mimeMessage
* the mime message to work on
* @param multipartMode
* which kind of multipart message to create (MIXED, RELATED,
* MIXED_RELATED, or NO)
* @param encoding
* the character encoding to use for the message
* @throws MessagingException
* if multipart creation failed
* @see #MULTIPART_MODE_NO
* @see #MULTIPART_MODE_MIXED
* @see #MULTIPART_MODE_RELATED
* @see #MULTIPART_MODE_MIXED_RELATED
*/
public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode, @Nullable String encoding) throws MessagingException {
this.mimeMessage = mimeMessage;
createMimeMultiparts(mimeMessage, multipartMode);
this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage));
this.fileTypeMap = getDefaultFileTypeMap(mimeMessage);
}
/**
* Return the underlying MimeMessage object.
*/
public final MimeMessage getMimeMessage() {
return this.mimeMessage;
}
/**
* Determine the MimeMultipart objects to use, which will be used to store
* attachments on the one hand and text(s) and inline elements on the other
* hand.
*
* Texts and inline elements can either be stored in the root element itself
* (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED) or in a nested element
* rather than the root element directly (MULTIPART_MODE_MIXED_RELATED).
*
* By default, the root MimeMultipart element will be of type "mixed"
* (MULTIPART_MODE_MIXED) or "related" (MULTIPART_MODE_RELATED). The main
* multipart element will either be added as nested element of type
* "related" (MULTIPART_MODE_MIXED_RELATED) or be identical to the root
* element itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED).
*
* @param mimeMessage
* the MimeMessage object to add the root MimeMultipart object to
* @param multipartMode
* the multipart mode, as passed into the constructor (MIXED,
* RELATED, MIXED_RELATED, or NO)
* @throws MessagingException
* if multipart creation failed
* @see #setMimeMultiparts
* @see #MULTIPART_MODE_NO
* @see #MULTIPART_MODE_MIXED
* @see #MULTIPART_MODE_RELATED
* @see #MULTIPART_MODE_MIXED_RELATED
*/
protected void createMimeMultiparts(MimeMessage mimeMessage, int multipartMode) throws MessagingException {
switch (multipartMode) {
case MULTIPART_MODE_NO:
setMimeMultiparts(null, null);
break;
case MULTIPART_MODE_MIXED:
MimeMultipart mixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED);
mimeMessage.setContent(mixedMultipart);
setMimeMultiparts(mixedMultipart, mixedMultipart);
break;
case MULTIPART_MODE_RELATED:
MimeMultipart relatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED);
mimeMessage.setContent(relatedMultipart);
setMimeMultiparts(relatedMultipart, relatedMultipart);
break;
case MULTIPART_MODE_MIXED_RELATED:
MimeMultipart rootMixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED);
mimeMessage.setContent(rootMixedMultipart);
MimeMultipart nestedRelatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED);
MimeBodyPart relatedBodyPart = new MimeBodyPart();
relatedBodyPart.setContent(nestedRelatedMultipart);
rootMixedMultipart.addBodyPart(relatedBodyPart);
setMimeMultiparts(rootMixedMultipart, nestedRelatedMultipart);
break;
default:
throw new IllegalArgumentException("Only multipart modes MIXED_RELATED, RELATED and NO supported");
}
}
/**
* Set the given MimeMultipart objects for use by this MimeMessageHelper.
*
* @param root
* the root MimeMultipart object, which attachments will be added
* to; or {@code null} to indicate no multipart at all
* @param main
* the main MimeMultipart object, which text(s) and inline
* elements will be added to (can be the same as the root
* multipart object, or an element nested underneath the root
* multipart element)
*/
protected final void setMimeMultiparts(@Nullable MimeMultipart root, @Nullable MimeMultipart main) {
this.rootMimeMultipart = root;
this.mimeMultipart = main;
}
/**
* Return whether this helper is in multipart mode, i.e. whether it holds a
* multipart message.
*
* @see #MimeMessageHelper(MimeMessage, boolean)
*/
public final boolean isMultipart() {
return (this.rootMimeMultipart != null);
}
/**
* Return the root MIME "multipart/mixed" object, if any. Can be used to
* manually add attachments.
*
* This will be the direct content of the MimeMessage, in case of a
* multipart mail.
*
* @throws IllegalStateException
* if this helper is not in multipart mode
* @see #isMultipart
* @see #getMimeMessage
* @see javax.mail.internet.MimeMultipart#addBodyPart
*/
public final MimeMultipart getRootMimeMultipart() throws IllegalStateException {
if (this.rootMimeMultipart == null) {
throw new IllegalStateException("Not in multipart mode - "
+ "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag "
+ "if you need to set alternative texts or add inline elements or attachments.");
}
return this.rootMimeMultipart;
}
/**
* Return the underlying MIME "multipart/related" object, if any. Can be
* used to manually add body parts, inline elements, etc.
*
* This will be nested within the root MimeMultipart, in case of a multipart
* mail.
*
* @throws IllegalStateException
* if this helper is not in multipart mode
* @see #isMultipart
* @see #getRootMimeMultipart
* @see javax.mail.internet.MimeMultipart#addBodyPart
*/
public final MimeMultipart getMimeMultipart() throws IllegalStateException {
if (this.mimeMultipart == null) {
throw new IllegalStateException("Not in multipart mode - "
+ "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag "
+ "if you need to set alternative texts or add inline elements or attachments.");
}
return this.mimeMultipart;
}
/**
* Determine the default encoding for the given MimeMessage.
*
* @param mimeMessage
* the passed-in MimeMessage
* @return the default encoding associated with the MimeMessage, or
* {@code null} if none found
*/
@Nullable
protected String getDefaultEncoding(MimeMessage mimeMessage) {
if (mimeMessage instanceof SmartMimeMessage) {
return ((SmartMimeMessage) mimeMessage).getDefaultEncoding();
}
return null;
}
/**
* Return the specific character encoding used for this message, if any.
*/
@Nullable
public String getEncoding() {
return this.encoding;
}
/**
* Determine the default Java Activation FileTypeMap for the given
* MimeMessage.
*
* @param mimeMessage
* the passed-in MimeMessage
* @return the default FileTypeMap associated with the MimeMessage, or a
* default ConfigurableMimeFileTypeMap if none found for the message
* @see ConfigurableMimeFileTypeMap
*/
protected FileTypeMap getDefaultFileTypeMap(MimeMessage mimeMessage) {
if (mimeMessage instanceof SmartMimeMessage) {
FileTypeMap fileTypeMap = ((SmartMimeMessage) mimeMessage).getDefaultFileTypeMap();
if (fileTypeMap != null) {
return fileTypeMap;
}
}
ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap();
fileTypeMap.init();
return fileTypeMap;
}
/**
* Set the Java Activation Framework {@code FileTypeMap} to use for
* determining the content type of inline content and attachments that get
* added to the message.
*
* The default is the {@code FileTypeMap} that the underlying MimeMessage
* carries, if any, or the Activation Framework's default
* {@code FileTypeMap} instance else.
*
* @see #addInline
* @see #addAttachment
* @see #getDefaultFileTypeMap(javax.mail.internet.MimeMessage)
* @see JavaMailSenderImpl#setDefaultFileTypeMap
* @see javax.activation.FileTypeMap#getDefaultFileTypeMap
* @see ConfigurableMimeFileTypeMap
*/
public void setFileTypeMap(@Nullable FileTypeMap fileTypeMap) {
this.fileTypeMap = (fileTypeMap != null ? fileTypeMap : getDefaultFileTypeMap(getMimeMessage()));
}
/**
* Return the {@code FileTypeMap} used by this MimeMessageHelper.
*
* @see #setFileTypeMap
*/
public FileTypeMap getFileTypeMap() {
return this.fileTypeMap;
}
/**
* Set whether to encode attachment filenames passed to this helper's
* {@code #addAttachment} methods.
*
* The default is {@code false} for standard MIME behavior; turn this to
* {@code true} for compatibility with older email clients. On a related
* note, check out JavaMail's {@code mail.mime.encodefilename} system
* property.
*
* NOTE: The default changed to {@code false} in 5.3, in favor of
* JavaMail's standard {@code mail.mime.encodefilename} system property.
*
* @since 5.2.9
* @see #addAttachment(String, DataSource)
* @see MimeBodyPart#setFileName(String)
*/
public void setEncodeFilenames(boolean encodeFilenames) {
this.encodeFilenames = encodeFilenames;
}
/**
* Return whether to encode attachment filenames passed to this helper's
* {@code #addAttachment} methods.
*
* @since 5.2.9
* @see #setEncodeFilenames
*/
public boolean isEncodeFilenames() {
return this.encodeFilenames;
}
/**
* Set whether to validate all addresses which get passed to this helper.
*
* The default is {@code false}.
*
* @see #validateAddress
*/
public void setValidateAddresses(boolean validateAddresses) {
this.validateAddresses = validateAddresses;
}
/**
* Return whether this helper will validate all addresses passed to it.
*
* @see #setValidateAddresses
*/
public boolean isValidateAddresses() {
return this.validateAddresses;
}
/**
* Validate the given mail address. Called by all of MimeMessageHelper's
* address setters and adders.
*
* The default implementation invokes {@link InternetAddress#validate()},
* provided that address validation is activated for the helper instance.
*
* @param address
* the address to validate
* @throws AddressException
* if validation failed
* @see #isValidateAddresses()
* @see javax.mail.internet.InternetAddress#validate()
*/
protected void validateAddress(InternetAddress address) throws AddressException {
if (isValidateAddresses()) {
address.validate();
}
}
/**
* Validate all given mail addresses.
*
* The default implementation simply delegates to {@link #validateAddress}
* for each address.
*
* @param addresses
* the addresses to validate
* @throws AddressException
* if validation failed
* @see #validateAddress(InternetAddress)
*/
protected void validateAddresses(InternetAddress[] addresses) throws AddressException {
for (InternetAddress address : addresses) {
validateAddress(address);
}
}
public void setFrom(InternetAddress from) throws MessagingException {
Assert2.notNull(from, "From address must not be null");
validateAddress(from);
this.mimeMessage.setFrom(from);
}
public void setFrom(String from) throws MessagingException {
Assert2.notNull(from, "From address must not be null");
setFrom(parseAddress(from));
}
public void setFrom(String from, String personal) throws MessagingException, UnsupportedEncodingException {
Assert2.notNull(from, "From address must not be null");
setFrom(getEncoding() != null ? new InternetAddress(from, personal, getEncoding()) : new InternetAddress(from, personal));
}
public void setReplyTo(InternetAddress replyTo) throws MessagingException {
Assert2.notNull(replyTo, "Reply-to address must not be null");
validateAddress(replyTo);
this.mimeMessage.setReplyTo(new InternetAddress[] { replyTo });
}
public void setReplyTo(String replyTo) throws MessagingException {
Assert2.notNull(replyTo, "Reply-to address must not be null");
setReplyTo(parseAddress(replyTo));
}
public void setReplyTo(String replyTo, String personal) throws MessagingException, UnsupportedEncodingException {
Assert2.notNull(replyTo, "Reply-to address must not be null");
InternetAddress replyToAddress = (getEncoding() != null) ? new InternetAddress(replyTo, personal, getEncoding())
: new InternetAddress(replyTo, personal);
setReplyTo(replyToAddress);
}
public void setTo(InternetAddress to) throws MessagingException {
Assert2.notNull(to, "To address must not be null");
validateAddress(to);
this.mimeMessage.setRecipient(Message.RecipientType.TO, to);
}
public void setTo(InternetAddress[] to) throws MessagingException {
Assert2.notNull(to, "To address array must not be null");
validateAddresses(to);
this.mimeMessage.setRecipients(Message.RecipientType.TO, to);
}
public void setTo(String to) throws MessagingException {
Assert2.notNull(to, "To address must not be null");
setTo(parseAddress(to));
}
public void setTo(String[] to) throws MessagingException {
Assert2.notNull(to, "To address array must not be null");
InternetAddress[] addresses = new InternetAddress[to.length];
for (int i = 0; i < to.length; i++) {
addresses[i] = parseAddress(to[i]);
}
setTo(addresses);
}
public void addTo(InternetAddress to) throws MessagingException {
Assert2.notNull(to, "To address must not be null");
validateAddress(to);
this.mimeMessage.addRecipient(Message.RecipientType.TO, to);
}
public void addTo(String to) throws MessagingException {
Assert2.notNull(to, "To address must not be null");
addTo(parseAddress(to));
}
public void addTo(String to, String personal) throws MessagingException, UnsupportedEncodingException {
Assert2.notNull(to, "To address must not be null");
addTo(getEncoding() != null ? new InternetAddress(to, personal, getEncoding()) : new InternetAddress(to, personal));
}
public void setCc(InternetAddress cc) throws MessagingException {
Assert2.notNull(cc, "Cc address must not be null");
validateAddress(cc);
this.mimeMessage.setRecipient(Message.RecipientType.CC, cc);
}
public void setCc(InternetAddress[] cc) throws MessagingException {
Assert2.notNull(cc, "Cc address array must not be null");
validateAddresses(cc);
this.mimeMessage.setRecipients(Message.RecipientType.CC, cc);
}
public void setCc(String cc) throws MessagingException {
Assert2.notNull(cc, "Cc address must not be null");
setCc(parseAddress(cc));
}
public void setCc(String[] cc) throws MessagingException {
Assert2.notNull(cc, "Cc address array must not be null");
InternetAddress[] addresses = new InternetAddress[cc.length];
for (int i = 0; i < cc.length; i++) {
addresses[i] = parseAddress(cc[i]);
}
setCc(addresses);
}
public void addCc(InternetAddress cc) throws MessagingException {
Assert2.notNull(cc, "Cc address must not be null");
validateAddress(cc);
this.mimeMessage.addRecipient(Message.RecipientType.CC, cc);
}
public void addCc(String cc) throws MessagingException {
Assert2.notNull(cc, "Cc address must not be null");
addCc(parseAddress(cc));
}
public void addCc(String cc, String personal) throws MessagingException, UnsupportedEncodingException {
Assert2.notNull(cc, "Cc address must not be null");
addCc(getEncoding() != null ? new InternetAddress(cc, personal, getEncoding()) : new InternetAddress(cc, personal));
}
public void setBcc(InternetAddress bcc) throws MessagingException {
Assert2.notNull(bcc, "Bcc address must not be null");
validateAddress(bcc);
this.mimeMessage.setRecipient(Message.RecipientType.BCC, bcc);
}
public void setBcc(InternetAddress[] bcc) throws MessagingException {
Assert2.notNull(bcc, "Bcc address array must not be null");
validateAddresses(bcc);
this.mimeMessage.setRecipients(Message.RecipientType.BCC, bcc);
}
public void setBcc(String bcc) throws MessagingException {
Assert2.notNull(bcc, "Bcc address must not be null");
setBcc(parseAddress(bcc));
}
public void setBcc(String[] bcc) throws MessagingException {
Assert2.notNull(bcc, "Bcc address array must not be null");
InternetAddress[] addresses = new InternetAddress[bcc.length];
for (int i = 0; i < bcc.length; i++) {
addresses[i] = parseAddress(bcc[i]);
}
setBcc(addresses);
}
public void addBcc(InternetAddress bcc) throws MessagingException {
Assert2.notNull(bcc, "Bcc address must not be null");
validateAddress(bcc);
this.mimeMessage.addRecipient(Message.RecipientType.BCC, bcc);
}
public void addBcc(String bcc) throws MessagingException {
Assert2.notNull(bcc, "Bcc address must not be null");
addBcc(parseAddress(bcc));
}
public void addBcc(String bcc, String personal) throws MessagingException, UnsupportedEncodingException {
Assert2.notNull(bcc, "Bcc address must not be null");
addBcc(getEncoding() != null ? new InternetAddress(bcc, personal, getEncoding()) : new InternetAddress(bcc, personal));
}
private InternetAddress parseAddress(String address) throws MessagingException {
InternetAddress[] parsed = InternetAddress.parse(address);
if (parsed.length != 1) {
throw new AddressException("Illegal address", address);
}
InternetAddress raw = parsed[0];
try {
return (getEncoding() != null ? new InternetAddress(raw.getAddress(), raw.getPersonal(), getEncoding()) : raw);
} catch (UnsupportedEncodingException ex) {
throw new MessagingException("Failed to parse embedded personal name to correct encoding", ex);
}
}
/**
* Set the priority ("X-Priority" header) of the message.
*
* @param priority
* the priority value; typically between 1 (highest) and 5
* (lowest)
* @throws MessagingException
* in case of errors
*/
public void setPriority(int priority) throws MessagingException {
this.mimeMessage.setHeader(HEADER_PRIORITY, Integer.toString(priority));
}
/**
* Set the sent-date of the message.
*
* @param sentDate
* the date to set (never {@code null})
* @throws MessagingException
* in case of errors
*/
public void setSentDate(Date sentDate) throws MessagingException {
Assert2.notNull(sentDate, "Sent date must not be null");
this.mimeMessage.setSentDate(sentDate);
}
/**
* Set the subject of the message, using the correct encoding.
*
* @param subject
* the subject text
* @throws MessagingException
* in case of errors
*/
public void setSubject(String subject) throws MessagingException {
Assert2.notNull(subject, "Subject must not be null");
if (getEncoding() != null) {
this.mimeMessage.setSubject(subject, getEncoding());
} else {
this.mimeMessage.setSubject(subject);
}
}
/**
* Set the given text directly as content in non-multipart mode or as
* default body part in multipart mode. Always applies the default content
* type "text/plain".
*
* NOTE: Invoke {@link #addInline} after {@code setText};
* else, mail readers might not be able to resolve inline references
* correctly.
*
* @param text
* the text for the message
* @throws MessagingException
* in case of errors
*/
public void setText(String text) throws MessagingException {
setText(text, false);
}
/**
* Set the given text directly as content in non-multipart mode or as
* default body part in multipart mode. The "html" flag determines the
* content type to apply.
*
* NOTE: Invoke {@link #addInline} after {@code setText};
* else, mail readers might not be able to resolve inline references
* correctly.
*
* @param text
* the text for the message
* @param html
* whether to apply content type "text/html" for an HTML mail,
* using default content type ("text/plain") else
* @throws MessagingException
* in case of errors
*/
public void setText(String text, boolean html) throws MessagingException {
Assert2.notNull(text, "Text must not be null");
MimePart partToUse;
if (isMultipart()) {
partToUse = getMainPart();
} else {
partToUse = this.mimeMessage;
}
if (html) {
setHtmlTextToMimePart(partToUse, text);
} else {
setPlainTextToMimePart(partToUse, text);
}
}
/**
* Set the given plain text and HTML text as alternatives, offering both
* options to the email client. Requires multipart mode.
*
* NOTE: Invoke {@link #addInline} after {@code setText};
* else, mail readers might not be able to resolve inline references
* correctly.
*
* @param plainText
* the plain text for the message
* @param htmlText
* the HTML text for the message
* @throws MessagingException
* in case of errors
*/
public void setText(String plainText, String htmlText) throws MessagingException {
Assert2.notNull(plainText, "Plain text must not be null");
Assert2.notNull(htmlText, "HTML text must not be null");
MimeMultipart messageBody = new MimeMultipart(MULTIPART_SUBTYPE_ALTERNATIVE);
getMainPart().setContent(messageBody, CONTENT_TYPE_ALTERNATIVE);
// Create the plain text part of the message.
MimeBodyPart plainTextPart = new MimeBodyPart();
setPlainTextToMimePart(plainTextPart, plainText);
messageBody.addBodyPart(plainTextPart);
// Create the HTML text part of the message.
MimeBodyPart htmlTextPart = new MimeBodyPart();
setHtmlTextToMimePart(htmlTextPart, htmlText);
messageBody.addBodyPart(htmlTextPart);
}
private MimeBodyPart getMainPart() throws MessagingException {
MimeMultipart mimeMultipart = getMimeMultipart();
MimeBodyPart bodyPart = null;
for (int i = 0; i < mimeMultipart.getCount(); i++) {
BodyPart bp = mimeMultipart.getBodyPart(i);
if (bp.getFileName() == null) {
bodyPart = (MimeBodyPart) bp;
}
}
if (bodyPart == null) {
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeMultipart.addBodyPart(mimeBodyPart);
bodyPart = mimeBodyPart;
}
return bodyPart;
}
private void setPlainTextToMimePart(MimePart mimePart, String text) throws MessagingException {
if (getEncoding() != null) {
mimePart.setText(text, getEncoding());
} else {
mimePart.setText(text);
}
}
private void setHtmlTextToMimePart(MimePart mimePart, String text) throws MessagingException {
if (getEncoding() != null) {
mimePart.setContent(text, CONTENT_TYPE_HTML + CONTENT_TYPE_CHARSET_SUFFIX + getEncoding());
} else {
mimePart.setContent(text, CONTENT_TYPE_HTML);
}
}
/**
* Add an inline element to the MimeMessage, taking the content from a
* {@code javax.activation.DataSource}.
*
* Note that the InputStream returned by the DataSource implementation needs
* to be a fresh one on each call, as JavaMail will invoke
* {@code getInputStream()} multiple times.
*
* NOTE: Invoke {@code addInline} after {@link #setText};
* else, mail readers might not be able to resolve inline references
* correctly.
*
* @param contentId
* the content ID to use. Will end up as "Content-ID" header in
* the body part, surrounded by angle brackets: e.g. "myId"
* → "<myId>". Can be referenced in HTML source via
* src="cid:myId" expressions.
* @param dataSource
* the {@code javax.activation.DataSource} to take the content
* from, determining the InputStream and the content type
* @throws MessagingException
* in case of errors
* @see #addInline(String, java.io.File)
* @see #addInline(String, org.springframework.core.io.Resource)
*/
public void addInline(String contentId, DataSource dataSource) throws MessagingException {
Assert2.notNull(contentId, "Content ID must not be null");
Assert2.notNull(dataSource, "DataSource must not be null");
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setDisposition(MimeBodyPart.INLINE);
mimeBodyPart.setContentID("<" + contentId + ">");
mimeBodyPart.setDataHandler(new DataHandler(dataSource));
getMimeMultipart().addBodyPart(mimeBodyPart);
}
/**
* Add an inline element to the MimeMessage, taking the content from a
* {@code java.io.File}.
*
* The content type will be determined by the name of the given content
* file. Do not use this for temporary files with arbitrary filenames
* (possibly ending in ".tmp" or the like)!
*
* NOTE: Invoke {@code addInline} after {@link #setText};
* else, mail readers might not be able to resolve inline references
* correctly.
*
* @param contentId
* the content ID to use. Will end up as "Content-ID" header in
* the body part, surrounded by angle brackets: e.g. "myId"
* → "<myId>". Can be referenced in HTML source via
* src="cid:myId" expressions.
* @param file
* the File resource to take the content from
* @throws MessagingException
* in case of errors
* @see #setText
* @see #addInline(String, org.springframework.core.io.Resource)
* @see #addInline(String, javax.activation.DataSource)
*/
public void addInline(String contentId, File file) throws MessagingException {
Assert2.notNull(file, "File must not be null");
FileDataSource dataSource = new FileDataSource(file);
dataSource.setFileTypeMap(getFileTypeMap());
addInline(contentId, dataSource);
}
/**
* Add an inline element to the MimeMessage, taking the content from a
* {@code org.springframework.core.io.Resource}.
*
* The content type will be determined by the name of the given content
* file. Do not use this for temporary files with arbitrary filenames
* (possibly ending in ".tmp" or the like)!
*
* Note that the InputStream returned by the Resource implementation needs
* to be a fresh one on each call, as JavaMail will invoke
* {@code getInputStream()} multiple times.
*
* NOTE: Invoke {@code addInline} after {@link #setText};
* else, mail readers might not be able to resolve inline references
* correctly.
*
* @param contentId
* the content ID to use. Will end up as "Content-ID" header in
* the body part, surrounded by angle brackets: e.g. "myId"
* → "<myId>". Can be referenced in HTML source via
* src="cid:myId" expressions.
* @param resource
* the resource to take the content from
* @throws MessagingException
* in case of errors
* @see #setText
* @see #addInline(String, java.io.File)
* @see #addInline(String, javax.activation.DataSource)
*/
public void addInline(String contentId, StreamResource resource) throws MessagingException {
Assert2.notNull(resource, "Resource must not be null");
String contentType = getFileTypeMap().getContentType(resource.getFilename());
addInline(contentId, resource, contentType);
}
/**
* Add an inline element to the MimeMessage, taking the content from an
* {@code org.springframework.core.InputStreamResource}, and specifying the
* content type explicitly.
*
* You can determine the content type for any given filename via a Java
* Activation Framework's FileTypeMap, for example the one held by this
* helper.
*
* Note that the InputStream returned by the InputStreamSource
* implementation needs to be a fresh one on each call, as JavaMail
* will invoke {@code getInputStream()} multiple times.
*
* NOTE: Invoke {@code addInline} after {@code setText}; else,
* mail readers might not be able to resolve inline references correctly.
*
* @param contentId
* the content ID to use. Will end up as "Content-ID" header in
* the body part, surrounded by angle brackets: e.g. "myId"
* → "<myId>". Can be referenced in HTML source via
* src="cid:myId" expressions.
* @param inputStreamSource
* the resource to take the content from
* @param contentType
* the content type to use for the element
* @throws MessagingException
* in case of errors
* @see #setText
* @see #getFileTypeMap
* @see #addInline(String, org.springframework.core.io.Resource)
* @see #addInline(String, javax.activation.DataSource)
*/
public void addInline(String contentId, StreamResource inputStreamSource, String contentType) throws MessagingException {
Assert2.notNull(inputStreamSource, "InputStreamSource must not be null");
if (inputStreamSource instanceof StreamResource && ((StreamResource) inputStreamSource).isOpen()) {
throw new IllegalArgumentException("Passed-in Resource contains an open stream: invalid argument. "
+ "JavaMail requires an InputStreamSource that creates a fresh stream for every call.");
}
DataSource dataSource = createDataSource(inputStreamSource, contentType, "inline");
addInline(contentId, dataSource);
}
/**
* Add an attachment to the MimeMessage, taking the content from a
* {@code javax.activation.DataSource}.
*
* Note that the InputStream returned by the DataSource implementation needs
* to be a fresh one on each call, as JavaMail will invoke
* {@code getInputStream()} multiple times.
*
* @param attachmentFilename
* the name of the attachment as it will appear in the mail (the
* content type will be determined by this)
* @param dataSource
* the {@code javax.activation.DataSource} to take the content
* from, determining the InputStream and the content type
* @throws MessagingException
* in case of errors
* @see #addAttachment(String,
* org.springframework.core.io.InputStreamSource)
* @see #addAttachment(String, java.io.File)
*/
public void addAttachment(String attachmentFilename, DataSource dataSource) throws MessagingException {
Assert2.notNull(attachmentFilename, "Attachment filename must not be null");
Assert2.notNull(dataSource, "DataSource must not be null");
try {
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setDisposition(MimeBodyPart.ATTACHMENT);
mimeBodyPart.setFileName(isEncodeFilenames() ? MimeUtility.encodeText(attachmentFilename) : attachmentFilename);
mimeBodyPart.setDataHandler(new DataHandler(dataSource));
getRootMimeMultipart().addBodyPart(mimeBodyPart);
} catch (UnsupportedEncodingException ex) {
throw new MessagingException("Failed to encode attachment filename", ex);
}
}
/**
* Add an attachment to the MimeMessage, taking the content from a
* {@code java.io.File}.
*
* The content type will be determined by the name of the given content
* file. Do not use this for temporary files with arbitrary filenames
* (possibly ending in ".tmp" or the like)!
*
* @param attachmentFilename
* the name of the attachment as it will appear in the mail
* @param file
* the File resource to take the content from
* @throws MessagingException
* in case of errors
* @see #addAttachment(String,
* org.springframework.core.io.InputStreamSource)
* @see #addAttachment(String, javax.activation.DataSource)
*/
public void addAttachment(String attachmentFilename, File file) throws MessagingException {
Assert2.notNull(file, "File must not be null");
FileDataSource dataSource = new FileDataSource(file);
dataSource.setFileTypeMap(getFileTypeMap());
addAttachment(attachmentFilename, dataSource);
}
/**
* Add an attachment to the MimeMessage, taking the content from an
* {@code org.springframework.core.io.InputStreamResource}.
*
* The content type will be determined by the given filename for the
* attachment. Thus, any content source will be fine, including temporary
* files with arbitrary filenames.
*
* Note that the InputStream returned by the InputStreamSource
* implementation needs to be a fresh one on each call, as JavaMail
* will invoke {@code getInputStream()} multiple times.
*
* @param attachmentFilename
* the name of the attachment as it will appear in the mail
* @param inputStreamSource
* the resource to take the content from (all of Spring's
* Resource implementations can be passed in here)
* @throws MessagingException
* in case of errors
* @see #addAttachment(String, java.io.File)
* @see #addAttachment(String, javax.activation.DataSource)
* @see org.springframework.core.io.Resource
*/
public void addAttachment(String attachmentFilename, StreamResource inputStreamSource) throws MessagingException {
String contentType = getFileTypeMap().getContentType(attachmentFilename);
addAttachment(attachmentFilename, inputStreamSource, contentType);
}
/**
* Add an attachment to the MimeMessage, taking the content from an
* {@code org.springframework.core.io.InputStreamResource}.
*
* Note that the InputStream returned by the InputStreamSource
* implementation needs to be a fresh one on each call, as JavaMail
* will invoke {@code getInputStream()} multiple times.
*
* @param attachmentFilename
* the name of the attachment as it will appear in the mail
* @param inputStreamSource
* the resource to take the content from (all of Spring's
* Resource implementations can be passed in here)
* @param contentType
* the content type to use for the element
* @throws MessagingException
* in case of errors
* @see #addAttachment(String, java.io.File)
* @see #addAttachment(String, javax.activation.DataSource)
* @see org.springframework.core.io.Resource
*/
public void addAttachment(String attachmentFilename, StreamResource inputStreamSource, String contentType)
throws MessagingException {
Assert2.notNull(inputStreamSource, "InputStreamSource must not be null");
if (inputStreamSource instanceof StreamResource && ((StreamResource) inputStreamSource).isOpen()) {
throw new IllegalArgumentException("Passed-in Resource contains an open stream: invalid argument. "
+ "JavaMail requires an InputStreamSource that creates a fresh stream for every call.");
}
DataSource dataSource = createDataSource(inputStreamSource, contentType, attachmentFilename);
addAttachment(attachmentFilename, dataSource);
}
/**
* Create an Activation Framework DataSource for the given
* InputStreamSource.
*
* @param inputStreamSource
* the InputStreamSource (typically a Spring Resource)
* @param contentType
* the content type
* @param name
* the name of the DataSource
* @return the Activation Framework DataSource
*/
protected DataSource createDataSource(final StreamResource inputStreamSource, final String contentType, final String name) {
return new DataSource() {
@Override
public InputStream getInputStream() throws IOException {
return inputStreamSource.getInputStream();
}
@Override
public OutputStream getOutputStream() {
throw new UnsupportedOperationException("Read-only javax.activation.DataSource");
}
@Override
public String getContentType() {
return contentType;
}
@Override
public String getName() {
return name;
}
};
}
public static class SmartMimeMessage extends MimeMessage {
@Nullable
private final String defaultEncoding;
@Nullable
private final FileTypeMap defaultFileTypeMap;
/**
* Create a new SmartMimeMessage.
*
* @param session
* the JavaMail Session to create the message for
* @param defaultEncoding
* the default encoding, or {@code null} if none
* @param defaultFileTypeMap
* the default FileTypeMap, or {@code null} if none
*/
public SmartMimeMessage(Session session, @Nullable String defaultEncoding, @Nullable FileTypeMap defaultFileTypeMap) {
super(session);
this.defaultEncoding = defaultEncoding;
this.defaultFileTypeMap = defaultFileTypeMap;
}
/**
* Return the default encoding of this message, or {@code null} if none.
*/
@Nullable
public final String getDefaultEncoding() {
return this.defaultEncoding;
}
/**
* Return the default FileTypeMap of this message, or {@code null} if
* none.
*/
@Nullable
public final FileTypeMap getDefaultFileTypeMap() {
return this.defaultFileTypeMap;
}
}
}