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