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