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}