View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.mail.javamail;
18  
19  import java.io.InputStream;
20  import java.util.ArrayList;
21  import java.util.Date;
22  import java.util.LinkedHashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Properties;
26  import javax.activation.FileTypeMap;
27  import javax.mail.AuthenticationFailedException;
28  import javax.mail.MessagingException;
29  import javax.mail.NoSuchProviderException;
30  import javax.mail.Session;
31  import javax.mail.Transport;
32  import javax.mail.internet.MimeMessage;
33  
34  import org.springframework.mail.MailAuthenticationException;
35  import org.springframework.mail.MailException;
36  import org.springframework.mail.MailParseException;
37  import org.springframework.mail.MailPreparationException;
38  import org.springframework.mail.MailSendException;
39  import org.springframework.mail.SimpleMailMessage;
40  import org.springframework.util.Assert;
41  
42  /**
43   * Production implementation of the {@link JavaMailSender} interface,
44   * supporting both JavaMail {@link MimeMessage MimeMessages} and Spring
45   * {@link SimpleMailMessage SimpleMailMessages}. Can also be used as a
46   * plain {@link org.springframework.mail.MailSender} implementation.
47   *
48   * <p>Allows for defining all settings locally as bean properties.
49   * Alternatively, a pre-configured JavaMail {@link javax.mail.Session} can be
50   * specified, possibly pulled from an application server's JNDI environment.
51   *
52   * <p>Non-default properties in this object will always override the settings
53   * in the JavaMail {@code Session}. Note that if overriding all values locally,
54   * there is no added value in setting a pre-configured {@code Session}.
55   *
56   * @author Dmitriy Kopylenko
57   * @author Juergen Hoeller
58   * @since 10.09.2003
59   * @see javax.mail.internet.MimeMessage
60   * @see javax.mail.Session
61   * @see #setSession
62   * @see #setJavaMailProperties
63   * @see #setHost
64   * @see #setPort
65   * @see #setUsername
66   * @see #setPassword
67   */
68  public class JavaMailSenderImpl implements JavaMailSender {
69  
70  	/** The default protocol: 'smtp' */
71  	public static final String DEFAULT_PROTOCOL = "smtp";
72  
73  	/** The default port: -1 */
74  	public static final int DEFAULT_PORT = -1;
75  
76  	private static final String HEADER_MESSAGE_ID = "Message-ID";
77  
78  
79  	private Properties javaMailProperties = new Properties();
80  
81  	private Session session;
82  
83  	private String protocol;
84  
85  	private String host;
86  
87  	private int port = DEFAULT_PORT;
88  
89  	private String username;
90  
91  	private String password;
92  
93  	private String defaultEncoding;
94  
95  	private FileTypeMap defaultFileTypeMap;
96  
97  
98  	/**
99  	 * Create a new instance of the {@code JavaMailSenderImpl} class.
100 	 * <p>Initializes the {@link #setDefaultFileTypeMap "defaultFileTypeMap"}
101 	 * property with a default {@link ConfigurableMimeFileTypeMap}.
102 	 */
103 	public JavaMailSenderImpl() {
104 		ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap();
105 		fileTypeMap.afterPropertiesSet();
106 		this.defaultFileTypeMap = fileTypeMap;
107 	}
108 
109 
110 	/**
111 	 * Set JavaMail properties for the {@code Session}.
112 	 * <p>A new {@code Session} will be created with those properties.
113 	 * Use either this method or {@link #setSession}, but not both.
114 	 * <p>Non-default properties in this instance will override given
115 	 * JavaMail properties.
116 	 */
117 	public void setJavaMailProperties(Properties javaMailProperties) {
118 		this.javaMailProperties = javaMailProperties;
119 		synchronized (this) {
120 			this.session = null;
121 		}
122 	}
123 
124 	/**
125 	 * Allow Map access to the JavaMail properties of this sender,
126 	 * with the option to add or override specific entries.
127 	 * <p>Useful for specifying entries directly, for example via
128 	 * "javaMailProperties[mail.smtp.auth]".
129 	 */
130 	public Properties getJavaMailProperties() {
131 		return this.javaMailProperties;
132 	}
133 
134 	/**
135 	 * Set the JavaMail {@code Session}, possibly pulled from JNDI.
136 	 * <p>Default is a new {@code Session} without defaults, that is
137 	 * completely configured via this instance's properties.
138 	 * <p>If using a pre-configured {@code Session}, non-default properties
139 	 * in this instance will override the settings in the {@code Session}.
140 	 * @see #setJavaMailProperties
141 	 */
142 	public synchronized void setSession(Session session) {
143 		Assert.notNull(session, "Session must not be null");
144 		this.session = session;
145 	}
146 
147 	/**
148 	 * Return the JavaMail {@code Session},
149 	 * lazily initializing it if hasn't been specified explicitly.
150 	 */
151 	public synchronized Session getSession() {
152 		if (this.session == null) {
153 			this.session = Session.getInstance(this.javaMailProperties);
154 		}
155 		return this.session;
156 	}
157 
158 	/**
159 	 * Set the mail protocol. Default is "smtp".
160 	 */
161 	public void setProtocol(String protocol) {
162 		this.protocol = protocol;
163 	}
164 
165 	/**
166 	 * Return the mail protocol.
167 	 */
168 	public String getProtocol() {
169 		return this.protocol;
170 	}
171 
172 	/**
173 	 * Set the mail server host, typically an SMTP host.
174 	 * <p>Default is the default host of the underlying JavaMail Session.
175 	 */
176 	public void setHost(String host) {
177 		this.host = host;
178 	}
179 
180 	/**
181 	 * Return the mail server host.
182 	 */
183 	public String getHost() {
184 		return this.host;
185 	}
186 
187 	/**
188 	 * Set the mail server port.
189 	 * <p>Default is {@link #DEFAULT_PORT}, letting JavaMail use the default
190 	 * SMTP port (25).
191 	*/
192 	public void setPort(int port) {
193 		this.port = port;
194 	}
195 
196 	/**
197 	 * Return the mail server port.
198 	 */
199 	public int getPort() {
200 		return this.port;
201 	}
202 
203 	/**
204 	 * Set the username for the account at the mail host, if any.
205 	 * <p>Note that the underlying JavaMail {@code Session} has to be
206 	 * configured with the property {@code "mail.smtp.auth"} set to
207 	 * {@code true}, else the specified username will not be sent to the
208 	 * mail server by the JavaMail runtime. If you are not explicitly passing
209 	 * in a {@code Session} to use, simply specify this setting via
210 	 * {@link #setJavaMailProperties}.
211 	 * @see #setSession
212 	 * @see #setPassword
213 	 */
214 	public void setUsername(String username) {
215 		this.username = username;
216 	}
217 
218 	/**
219 	 * Return the username for the account at the mail host.
220 	 */
221 	public String getUsername() {
222 		return this.username;
223 	}
224 
225 	/**
226 	 * Set the password for the account at the mail host, if any.
227 	 * <p>Note that the underlying JavaMail {@code Session} has to be
228 	 * configured with the property {@code "mail.smtp.auth"} set to
229 	 * {@code true}, else the specified password will not be sent to the
230 	 * mail server by the JavaMail runtime. If you are not explicitly passing
231 	 * in a {@code Session} to use, simply specify this setting via
232 	 * {@link #setJavaMailProperties}.
233 	 * @see #setSession
234 	 * @see #setUsername
235 	 */
236 	public void setPassword(String password) {
237 		this.password = password;
238 	}
239 
240 	/**
241 	 * Return the password for the account at the mail host.
242 	 */
243 	public String getPassword() {
244 		return this.password;
245 	}
246 
247 	/**
248 	 * Set the default encoding to use for {@link MimeMessage MimeMessages}
249 	 * created by this instance.
250 	 * <p>Such an encoding will be auto-detected by {@link MimeMessageHelper}.
251 	 */
252 	public void setDefaultEncoding(String defaultEncoding) {
253 		this.defaultEncoding = defaultEncoding;
254 	}
255 
256 	/**
257 	 * Return the default encoding for {@link MimeMessage MimeMessages},
258 	 * or {@code null} if none.
259 	 */
260 	public String getDefaultEncoding() {
261 		return this.defaultEncoding;
262 	}
263 
264 	/**
265 	 * Set the default Java Activation {@link FileTypeMap} to use for
266 	 * {@link MimeMessage MimeMessages} created by this instance.
267 	 * <p>A {@code FileTypeMap} specified here will be autodetected by
268 	 * {@link MimeMessageHelper}, avoiding the need to specify the
269 	 * {@code FileTypeMap} for each {@code MimeMessageHelper} instance.
270 	 * <p>For example, you can specify a custom instance of Spring's
271 	 * {@link ConfigurableMimeFileTypeMap} here. If not explicitly specified,
272 	 * a default {@code ConfigurableMimeFileTypeMap} will be used, containing
273 	 * an extended set of MIME type mappings (as defined by the
274 	 * {@code mime.types} file contained in the Spring jar).
275 	 * @see MimeMessageHelper#setFileTypeMap
276 	 */
277 	public void setDefaultFileTypeMap(FileTypeMap defaultFileTypeMap) {
278 		this.defaultFileTypeMap = defaultFileTypeMap;
279 	}
280 
281 	/**
282 	 * Return the default Java Activation {@link FileTypeMap} for
283 	 * {@link MimeMessage MimeMessages}, or {@code null} if none.
284 	 */
285 	public FileTypeMap getDefaultFileTypeMap() {
286 		return this.defaultFileTypeMap;
287 	}
288 
289 
290 	//---------------------------------------------------------------------
291 	// Implementation of MailSender
292 	//---------------------------------------------------------------------
293 
294 	@Override
295 	public void send(SimpleMailMessage simpleMessage) throws MailException {
296 		send(new SimpleMailMessage[] {simpleMessage});
297 	}
298 
299 	@Override
300 	public void send(SimpleMailMessage... simpleMessages) throws MailException {
301 		List<MimeMessage> mimeMessages = new ArrayList<MimeMessage>(simpleMessages.length);
302 		for (SimpleMailMessage simpleMessage : simpleMessages) {
303 			MimeMailMessage message = new MimeMailMessage(createMimeMessage());
304 			simpleMessage.copyTo(message);
305 			mimeMessages.add(message.getMimeMessage());
306 		}
307 		doSend(mimeMessages.toArray(new MimeMessage[mimeMessages.size()]), simpleMessages);
308 	}
309 
310 
311 	//---------------------------------------------------------------------
312 	// Implementation of JavaMailSender
313 	//---------------------------------------------------------------------
314 
315 	/**
316 	 * This implementation creates a SmartMimeMessage, holding the specified
317 	 * default encoding and default FileTypeMap. This special defaults-carrying
318 	 * message will be autodetected by {@link MimeMessageHelper}, which will use
319 	 * the carried encoding and FileTypeMap unless explicitly overridden.
320 	 * @see #setDefaultEncoding
321 	 * @see #setDefaultFileTypeMap
322 	 */
323 	@Override
324 	public MimeMessage createMimeMessage() {
325 		return new SmartMimeMessage(getSession(), getDefaultEncoding(), getDefaultFileTypeMap());
326 	}
327 
328 	@Override
329 	public MimeMessage createMimeMessage(InputStream contentStream) throws MailException {
330 		try {
331 			return new MimeMessage(getSession(), contentStream);
332 		}
333 		catch (Exception ex) {
334 			throw new MailParseException("Could not parse raw MIME content", ex);
335 		}
336 	}
337 
338 	@Override
339 	public void send(MimeMessage mimeMessage) throws MailException {
340 		send(new MimeMessage[] {mimeMessage});
341 	}
342 
343 	@Override
344 	public void send(MimeMessage... mimeMessages) throws MailException {
345 		doSend(mimeMessages, null);
346 	}
347 
348 	@Override
349 	public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException {
350 		send(new MimeMessagePreparator[] {mimeMessagePreparator});
351 	}
352 
353 	@Override
354 	public void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException {
355 		try {
356 			List<MimeMessage> mimeMessages = new ArrayList<MimeMessage>(mimeMessagePreparators.length);
357 			for (MimeMessagePreparator preparator : mimeMessagePreparators) {
358 				MimeMessage mimeMessage = createMimeMessage();
359 				preparator.prepare(mimeMessage);
360 				mimeMessages.add(mimeMessage);
361 			}
362 			send(mimeMessages.toArray(new MimeMessage[mimeMessages.size()]));
363 		}
364 		catch (MailException ex) {
365 			throw ex;
366 		}
367 		catch (MessagingException ex) {
368 			throw new MailParseException(ex);
369 		}
370 		catch (Exception ex) {
371 			throw new MailPreparationException(ex);
372 		}
373 	}
374 
375 
376 	/**
377 	 * Actually send the given array of MimeMessages via JavaMail.
378 	 * @param mimeMessages MimeMessage objects to send
379 	 * @param originalMessages corresponding original message objects
380 	 * that the MimeMessages have been created from (with same array
381 	 * length and indices as the "mimeMessages" array), if any
382 	 * @throws org.springframework.mail.MailAuthenticationException
383 	 * in case of authentication failure
384 	 * @throws org.springframework.mail.MailSendException
385 	 * in case of failure when sending a message
386 	 */
387 	protected void doSend(MimeMessage[] mimeMessages, Object[] originalMessages) throws MailException {
388 		Map<Object, Exception> failedMessages = new LinkedHashMap<Object, Exception>();
389 		Transport transport = null;
390 
391 		try {
392 			for (int i = 0; i < mimeMessages.length; i++) {
393 
394 				// Check transport connection first...
395 				if (transport == null || !transport.isConnected()) {
396 					if (transport != null) {
397 						try {
398 							transport.close();
399 						}
400 						catch (Exception ex) {
401 							// Ignore - we're reconnecting anyway
402 						}
403 						transport = null;
404 					}
405 					try {
406 						transport = connectTransport();
407 					}
408 					catch (AuthenticationFailedException ex) {
409 						throw new MailAuthenticationException(ex);
410 					}
411 					catch (Exception ex) {
412 						// Effectively, all remaining messages failed...
413 						for (int j = i; j < mimeMessages.length; j++) {
414 							Object original = (originalMessages != null ? originalMessages[j] : mimeMessages[j]);
415 							failedMessages.put(original, ex);
416 						}
417 						throw new MailSendException("Mail server connection failed", ex, failedMessages);
418 					}
419 				}
420 
421 				// Send message via current transport...
422 				MimeMessage mimeMessage = mimeMessages[i];
423 				try {
424 					if (mimeMessage.getSentDate() == null) {
425 						mimeMessage.setSentDate(new Date());
426 					}
427 					String messageId = mimeMessage.getMessageID();
428 					mimeMessage.saveChanges();
429 					if (messageId != null) {
430 						// Preserve explicitly specified message id...
431 						mimeMessage.setHeader(HEADER_MESSAGE_ID, messageId);
432 					}
433 					transport.sendMessage(mimeMessage, mimeMessage.getAllRecipients());
434 				}
435 				catch (Exception ex) {
436 					Object original = (originalMessages != null ? originalMessages[i] : mimeMessage);
437 					failedMessages.put(original, ex);
438 				}
439 			}
440 		}
441 		finally {
442 			try {
443 				if (transport != null) {
444 					transport.close();
445 				}
446 			}
447 			catch (Exception ex) {
448 				if (!failedMessages.isEmpty()) {
449 					throw new MailSendException("Failed to close server connection after message failures", ex,
450 							failedMessages);
451 				}
452 				else {
453 					throw new MailSendException("Failed to close server connection after message sending", ex);
454 				}
455 			}
456 		}
457 
458 		if (!failedMessages.isEmpty()) {
459 			throw new MailSendException(failedMessages);
460 		}
461 	}
462 
463 	/**
464 	 * Obtain and connect a Transport from the underlying JavaMail Session,
465 	 * passing in the specified host, port, username, and password.
466 	 * @return the connected Transport object
467 	 * @throws MessagingException if the connect attempt failed
468 	 * @since 4.1.2
469 	 * @see #getTransport
470 	 * @see #getHost()
471 	 * @see #getPort()
472 	 * @see #getUsername()
473 	 * @see #getPassword()
474 	 */
475 	protected Transport connectTransport() throws MessagingException {
476 		String username = getUsername();
477 		String password = getPassword();
478 		if ("".equals(username)) {  // probably from a placeholder
479 			username = null;
480 			if ("".equals(password)) {  // in conjunction with "" username, this means no password to use
481 				password = null;
482 			}
483 		}
484 
485 		Transport transport = getTransport(getSession());
486 		transport.connect(getHost(), getPort(), username, password);
487 		return transport;
488 	}
489 
490 	/**
491 	 * Obtain a Transport object from the given JavaMail Session,
492 	 * using the configured protocol.
493 	 * <p>Can be overridden in subclasses, e.g. to return a mock Transport object.
494 	 * @see javax.mail.Session#getTransport(String)
495 	 * @see #getSession()
496 	 * @see #getProtocol()
497 	 */
498 	protected Transport getTransport(Session session) throws NoSuchProviderException {
499 		String protocol	= getProtocol();
500 		if (protocol == null) {
501 			protocol = session.getProperty("mail.transport.protocol");
502 			if (protocol == null) {
503 				protocol = DEFAULT_PROTOCOL;
504 			}
505 		}
506 		return session.getTransport(protocol);
507 	}
508 
509 }