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