001/*
002 * Copyright 2002-2018 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.InputStream;
020import java.util.ArrayList;
021import java.util.Date;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Properties;
026
027import javax.activation.FileTypeMap;
028import javax.mail.Address;
029import javax.mail.AuthenticationFailedException;
030import javax.mail.MessagingException;
031import javax.mail.NoSuchProviderException;
032import javax.mail.Session;
033import javax.mail.Transport;
034import javax.mail.internet.MimeMessage;
035
036import org.springframework.lang.Nullable;
037import org.springframework.mail.MailAuthenticationException;
038import org.springframework.mail.MailException;
039import org.springframework.mail.MailParseException;
040import org.springframework.mail.MailPreparationException;
041import org.springframework.mail.MailSendException;
042import org.springframework.mail.SimpleMailMessage;
043import org.springframework.util.Assert;
044
045/**
046 * Production implementation of the {@link JavaMailSender} interface,
047 * supporting both JavaMail {@link MimeMessage MimeMessages} and Spring
048 * {@link SimpleMailMessage SimpleMailMessages}. Can also be used as a
049 * plain {@link org.springframework.mail.MailSender} implementation.
050 *
051 * <p>Allows for defining all settings locally as bean properties.
052 * Alternatively, a pre-configured JavaMail {@link javax.mail.Session} can be
053 * specified, possibly pulled from an application server's JNDI environment.
054 *
055 * <p>Non-default properties in this object will always override the settings
056 * in the JavaMail {@code Session}. Note that if overriding all values locally,
057 * there is no added value in setting a pre-configured {@code Session}.
058 *
059 * @author Dmitriy Kopylenko
060 * @author Juergen Hoeller
061 * @since 10.09.2003
062 * @see javax.mail.internet.MimeMessage
063 * @see javax.mail.Session
064 * @see #setSession
065 * @see #setJavaMailProperties
066 * @see #setHost
067 * @see #setPort
068 * @see #setUsername
069 * @see #setPassword
070 */
071public class JavaMailSenderImpl implements JavaMailSender {
072
073        /** The default protocol: 'smtp'. */
074        public static final String DEFAULT_PROTOCOL = "smtp";
075
076        /** The default port: -1. */
077        public static final int DEFAULT_PORT = -1;
078
079        private static final String HEADER_MESSAGE_ID = "Message-ID";
080
081
082        private Properties javaMailProperties = new Properties();
083
084        @Nullable
085        private Session session;
086
087        @Nullable
088        private String protocol;
089
090        @Nullable
091        private String host;
092
093        private int port = DEFAULT_PORT;
094
095        @Nullable
096        private String username;
097
098        @Nullable
099        private String password;
100
101        @Nullable
102        private String defaultEncoding;
103
104        @Nullable
105        private FileTypeMap defaultFileTypeMap;
106
107
108        /**
109         * Create a new instance of the {@code JavaMailSenderImpl} class.
110         * <p>Initializes the {@link #setDefaultFileTypeMap "defaultFileTypeMap"}
111         * property with a default {@link ConfigurableMimeFileTypeMap}.
112         */
113        public JavaMailSenderImpl() {
114                ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap();
115                fileTypeMap.afterPropertiesSet();
116                this.defaultFileTypeMap = fileTypeMap;
117        }
118
119
120        /**
121         * Set JavaMail properties for the {@code Session}.
122         * <p>A new {@code Session} will be created with those properties.
123         * Use either this method or {@link #setSession}, but not both.
124         * <p>Non-default properties in this instance will override given
125         * JavaMail properties.
126         */
127        public void setJavaMailProperties(Properties javaMailProperties) {
128                this.javaMailProperties = javaMailProperties;
129                synchronized (this) {
130                        this.session = null;
131                }
132        }
133
134        /**
135         * Allow Map access to the JavaMail properties of this sender,
136         * with the option to add or override specific entries.
137         * <p>Useful for specifying entries directly, for example via
138         * "javaMailProperties[mail.smtp.auth]".
139         */
140        public Properties getJavaMailProperties() {
141                return this.javaMailProperties;
142        }
143
144        /**
145         * Set the JavaMail {@code Session}, possibly pulled from JNDI.
146         * <p>Default is a new {@code Session} without defaults, that is
147         * completely configured via this instance's properties.
148         * <p>If using a pre-configured {@code Session}, non-default properties
149         * in this instance will override the settings in the {@code Session}.
150         * @see #setJavaMailProperties
151         */
152        public synchronized void setSession(Session session) {
153                Assert.notNull(session, "Session must not be null");
154                this.session = session;
155        }
156
157        /**
158         * Return the JavaMail {@code Session},
159         * lazily initializing it if hasn't been specified explicitly.
160         */
161        public synchronized Session getSession() {
162                if (this.session == null) {
163                        this.session = Session.getInstance(this.javaMailProperties);
164                }
165                return this.session;
166        }
167
168        /**
169         * Set the mail protocol. Default is "smtp".
170         */
171        public void setProtocol(@Nullable String protocol) {
172                this.protocol = protocol;
173        }
174
175        /**
176         * Return the mail protocol.
177         */
178        @Nullable
179        public String getProtocol() {
180                return this.protocol;
181        }
182
183        /**
184         * Set the mail server host, typically an SMTP host.
185         * <p>Default is the default host of the underlying JavaMail Session.
186         */
187        public void setHost(@Nullable String host) {
188                this.host = host;
189        }
190
191        /**
192         * Return the mail server host.
193         */
194        @Nullable
195        public String getHost() {
196                return this.host;
197        }
198
199        /**
200         * Set the mail server port.
201         * <p>Default is {@link #DEFAULT_PORT}, letting JavaMail use the default
202         * SMTP port (25).
203        */
204        public void setPort(int port) {
205                this.port = port;
206        }
207
208        /**
209         * Return the mail server port.
210         */
211        public int getPort() {
212                return this.port;
213        }
214
215        /**
216         * Set the username for the account at the mail host, if any.
217         * <p>Note that the underlying JavaMail {@code Session} has to be
218         * configured with the property {@code "mail.smtp.auth"} set to
219         * {@code true}, else the specified username will not be sent to the
220         * mail server by the JavaMail runtime. If you are not explicitly passing
221         * in a {@code Session} to use, simply specify this setting via
222         * {@link #setJavaMailProperties}.
223         * @see #setSession
224         * @see #setPassword
225         */
226        public void setUsername(@Nullable String username) {
227                this.username = username;
228        }
229
230        /**
231         * Return the username for the account at the mail host.
232         */
233        @Nullable
234        public String getUsername() {
235                return this.username;
236        }
237
238        /**
239         * Set the password for the account at the mail host, if any.
240         * <p>Note that the underlying JavaMail {@code Session} has to be
241         * configured with the property {@code "mail.smtp.auth"} set to
242         * {@code true}, else the specified password will not be sent to the
243         * mail server by the JavaMail runtime. If you are not explicitly passing
244         * in a {@code Session} to use, simply specify this setting via
245         * {@link #setJavaMailProperties}.
246         * @see #setSession
247         * @see #setUsername
248         */
249        public void setPassword(@Nullable String password) {
250                this.password = password;
251        }
252
253        /**
254         * Return the password for the account at the mail host.
255         */
256        @Nullable
257        public String getPassword() {
258                return this.password;
259        }
260
261        /**
262         * Set the default encoding to use for {@link MimeMessage MimeMessages}
263         * created by this instance.
264         * <p>Such an encoding will be auto-detected by {@link MimeMessageHelper}.
265         */
266        public void setDefaultEncoding(@Nullable String defaultEncoding) {
267                this.defaultEncoding = defaultEncoding;
268        }
269
270        /**
271         * Return the default encoding for {@link MimeMessage MimeMessages},
272         * or {@code null} if none.
273         */
274        @Nullable
275        public String getDefaultEncoding() {
276                return this.defaultEncoding;
277        }
278
279        /**
280         * Set the default Java Activation {@link FileTypeMap} to use for
281         * {@link MimeMessage MimeMessages} created by this instance.
282         * <p>A {@code FileTypeMap} specified here will be autodetected by
283         * {@link MimeMessageHelper}, avoiding the need to specify the
284         * {@code FileTypeMap} for each {@code MimeMessageHelper} instance.
285         * <p>For example, you can specify a custom instance of Spring's
286         * {@link ConfigurableMimeFileTypeMap} here. If not explicitly specified,
287         * a default {@code ConfigurableMimeFileTypeMap} will be used, containing
288         * an extended set of MIME type mappings (as defined by the
289         * {@code mime.types} file contained in the Spring jar).
290         * @see MimeMessageHelper#setFileTypeMap
291         */
292        public void setDefaultFileTypeMap(@Nullable FileTypeMap defaultFileTypeMap) {
293                this.defaultFileTypeMap = defaultFileTypeMap;
294        }
295
296        /**
297         * Return the default Java Activation {@link FileTypeMap} for
298         * {@link MimeMessage MimeMessages}, or {@code null} if none.
299         */
300        @Nullable
301        public FileTypeMap getDefaultFileTypeMap() {
302                return this.defaultFileTypeMap;
303        }
304
305
306        //---------------------------------------------------------------------
307        // Implementation of MailSender
308        //---------------------------------------------------------------------
309
310        @Override
311        public void send(SimpleMailMessage simpleMessage) throws MailException {
312                send(new SimpleMailMessage[] {simpleMessage});
313        }
314
315        @Override
316        public void send(SimpleMailMessage... simpleMessages) throws MailException {
317                List<MimeMessage> mimeMessages = new ArrayList<>(simpleMessages.length);
318                for (SimpleMailMessage simpleMessage : simpleMessages) {
319                        MimeMailMessage message = new MimeMailMessage(createMimeMessage());
320                        simpleMessage.copyTo(message);
321                        mimeMessages.add(message.getMimeMessage());
322                }
323                doSend(mimeMessages.toArray(new MimeMessage[0]), simpleMessages);
324        }
325
326
327        //---------------------------------------------------------------------
328        // Implementation of JavaMailSender
329        //---------------------------------------------------------------------
330
331        /**
332         * This implementation creates a SmartMimeMessage, holding the specified
333         * default encoding and default FileTypeMap. This special defaults-carrying
334         * message will be autodetected by {@link MimeMessageHelper}, which will use
335         * the carried encoding and FileTypeMap unless explicitly overridden.
336         * @see #setDefaultEncoding
337         * @see #setDefaultFileTypeMap
338         */
339        @Override
340        public MimeMessage createMimeMessage() {
341                return new SmartMimeMessage(getSession(), getDefaultEncoding(), getDefaultFileTypeMap());
342        }
343
344        @Override
345        public MimeMessage createMimeMessage(InputStream contentStream) throws MailException {
346                try {
347                        return new MimeMessage(getSession(), contentStream);
348                }
349                catch (Exception ex) {
350                        throw new MailParseException("Could not parse raw MIME content", ex);
351                }
352        }
353
354        @Override
355        public void send(MimeMessage mimeMessage) throws MailException {
356                send(new MimeMessage[] {mimeMessage});
357        }
358
359        @Override
360        public void send(MimeMessage... mimeMessages) throws MailException {
361                doSend(mimeMessages, null);
362        }
363
364        @Override
365        public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException {
366                send(new MimeMessagePreparator[] {mimeMessagePreparator});
367        }
368
369        @Override
370        public void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException {
371                try {
372                        List<MimeMessage> mimeMessages = new ArrayList<>(mimeMessagePreparators.length);
373                        for (MimeMessagePreparator preparator : mimeMessagePreparators) {
374                                MimeMessage mimeMessage = createMimeMessage();
375                                preparator.prepare(mimeMessage);
376                                mimeMessages.add(mimeMessage);
377                        }
378                        send(mimeMessages.toArray(new MimeMessage[0]));
379                }
380                catch (MailException ex) {
381                        throw ex;
382                }
383                catch (MessagingException ex) {
384                        throw new MailParseException(ex);
385                }
386                catch (Exception ex) {
387                        throw new MailPreparationException(ex);
388                }
389        }
390
391        /**
392         * Validate that this instance can connect to the server that it is configured
393         * for. Throws a {@link MessagingException} if the connection attempt failed.
394         */
395        public void testConnection() throws MessagingException {
396                Transport transport = null;
397                try {
398                        transport = connectTransport();
399                }
400                finally {
401                        if (transport != null) {
402                                transport.close();
403                        }
404                }
405        }
406
407        /**
408         * Actually send the given array of MimeMessages via JavaMail.
409         * @param mimeMessages the MimeMessage objects to send
410         * @param originalMessages corresponding original message objects
411         * that the MimeMessages have been created from (with same array
412         * length and indices as the "mimeMessages" array), if any
413         * @throws org.springframework.mail.MailAuthenticationException
414         * in case of authentication failure
415         * @throws org.springframework.mail.MailSendException
416         * in case of failure when sending a message
417         */
418        protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException {
419                Map<Object, Exception> failedMessages = new LinkedHashMap<>();
420                Transport transport = null;
421
422                try {
423                        for (int i = 0; i < mimeMessages.length; i++) {
424
425                                // Check transport connection first...
426                                if (transport == null || !transport.isConnected()) {
427                                        if (transport != null) {
428                                                try {
429                                                        transport.close();
430                                                }
431                                                catch (Exception ex) {
432                                                        // Ignore - we're reconnecting anyway
433                                                }
434                                                transport = null;
435                                        }
436                                        try {
437                                                transport = connectTransport();
438                                        }
439                                        catch (AuthenticationFailedException ex) {
440                                                throw new MailAuthenticationException(ex);
441                                        }
442                                        catch (Exception ex) {
443                                                // Effectively, all remaining messages failed...
444                                                for (int j = i; j < mimeMessages.length; j++) {
445                                                        Object original = (originalMessages != null ? originalMessages[j] : mimeMessages[j]);
446                                                        failedMessages.put(original, ex);
447                                                }
448                                                throw new MailSendException("Mail server connection failed", ex, failedMessages);
449                                        }
450                                }
451
452                                // Send message via current transport...
453                                MimeMessage mimeMessage = mimeMessages[i];
454                                try {
455                                        if (mimeMessage.getSentDate() == null) {
456                                                mimeMessage.setSentDate(new Date());
457                                        }
458                                        String messageId = mimeMessage.getMessageID();
459                                        mimeMessage.saveChanges();
460                                        if (messageId != null) {
461                                                // Preserve explicitly specified message id...
462                                                mimeMessage.setHeader(HEADER_MESSAGE_ID, messageId);
463                                        }
464                                        Address[] addresses = mimeMessage.getAllRecipients();
465                                        transport.sendMessage(mimeMessage, (addresses != null ? addresses : new Address[0]));
466                                }
467                                catch (Exception ex) {
468                                        Object original = (originalMessages != null ? originalMessages[i] : mimeMessage);
469                                        failedMessages.put(original, ex);
470                                }
471                        }
472                }
473                finally {
474                        try {
475                                if (transport != null) {
476                                        transport.close();
477                                }
478                        }
479                        catch (Exception ex) {
480                                if (!failedMessages.isEmpty()) {
481                                        throw new MailSendException("Failed to close server connection after message failures", ex,
482                                                        failedMessages);
483                                }
484                                else {
485                                        throw new MailSendException("Failed to close server connection after message sending", ex);
486                                }
487                        }
488                }
489
490                if (!failedMessages.isEmpty()) {
491                        throw new MailSendException(failedMessages);
492                }
493        }
494
495        /**
496         * Obtain and connect a Transport from the underlying JavaMail Session,
497         * passing in the specified host, port, username, and password.
498         * @return the connected Transport object
499         * @throws MessagingException if the connect attempt failed
500         * @since 4.1.2
501         * @see #getTransport
502         * @see #getHost()
503         * @see #getPort()
504         * @see #getUsername()
505         * @see #getPassword()
506         */
507        protected Transport connectTransport() throws MessagingException {
508                String username = getUsername();
509                String password = getPassword();
510                if ("".equals(username)) {  // probably from a placeholder
511                        username = null;
512                        if ("".equals(password)) {  // in conjunction with "" username, this means no password to use
513                                password = null;
514                        }
515                }
516
517                Transport transport = getTransport(getSession());
518                transport.connect(getHost(), getPort(), username, password);
519                return transport;
520        }
521
522        /**
523         * Obtain a Transport object from the given JavaMail Session,
524         * using the configured protocol.
525         * <p>Can be overridden in subclasses, e.g. to return a mock Transport object.
526         * @see javax.mail.Session#getTransport(String)
527         * @see #getSession()
528         * @see #getProtocol()
529         */
530        protected Transport getTransport(Session session) throws NoSuchProviderException {
531                String protocol = getProtocol();
532                if (protocol == null) {
533                        protocol = session.getProperty("mail.transport.protocol");
534                        if (protocol == null) {
535                                protocol = DEFAULT_PROTOCOL;
536                        }
537                }
538                return session.getTransport(protocol);
539        }
540
541}