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}