001package org.codemonkey.simplejavamail; 002 003import java.io.UnsupportedEncodingException; 004import java.util.Date; 005import java.util.Map; 006import java.util.Properties; 007 008import javax.activation.DataHandler; 009import javax.activation.DataSource; 010import javax.mail.Address; 011import javax.mail.Authenticator; 012import javax.mail.BodyPart; 013import javax.mail.Message; 014import javax.mail.MessagingException; 015import javax.mail.Part; 016import javax.mail.PasswordAuthentication; 017import javax.mail.Session; 018import javax.mail.Transport; 019import javax.mail.internet.InternetAddress; 020import javax.mail.internet.MimeBodyPart; 021import javax.mail.internet.MimeMessage; 022import javax.mail.internet.MimeMultipart; 023import javax.mail.internet.MimeUtility; 024 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028/** 029 * Mailing tool aimed for simplicity, for sending e-mails of any complexity. This includes e-mails with plain text and/or html content, 030 * embedded images and separate attachments, SMTP, SMTPS / SSL and SMTP + SSL<br> 031 * <br> 032 * This mailing tool abstracts the javax.mail API to a higher level easy to use API. For public use, this tool only works with {@link Email} 033 * instances. <br> 034 * <br> 035 * The e-mail message structure is built to work with all e-mail clients and has been tested with many different webclients as well as some 036 * mainstream client applications such as MS Outlook or Mozilla Thunderbird.<br> 037 * <br> 038 * Technically, the resulting email structure is as follows:<br> 039 * 040 * <pre> 041 * - root 042 * - related 043 * - alternative 044 * - mail text 045 * - mail html text 046 * - embedded images 047 * - attachments 048 * </pre> 049 * 050 * <br> 051 * Usage example:<br> 052 * 053 * <pre> 054 * Email email = new Email(); 055 * email.setFromAddress("lollypop", "lolly.pop@somemail.com"); 056 * email.addRecipient("Sugar Cane", "sugar.cane@candystore.org", RecipientType.TO); 057 * email.setText("We should meet up!!"); 058 * email.setTextHTML("<b>We should meet up!</b>"); 059 * email.setSubject("Hey"); 060 * new Mailer(preconfiguredMailSession).sendMail(email); 061 * // or: 062 * new Mailer("smtp.someserver.com", 25, "username", "password").sendMail(email); 063 * </pre> 064 * 065 * @author Benny Bottema 066 * @see MimeEmailMessageWrapper 067 * @see Email 068 */ 069public class Mailer { 070 071 private static final Logger logger = LoggerFactory.getLogger(Mailer.class); 072 073 /** 074 * Encoding used for setting body text, email address, headers, reply-to fields etc. ({@value #CHARACTER_ENCODING}). 075 */ 076 private static final String CHARACTER_ENCODING = "UTF-8"; 077 078 /** 079 * Used to actually send the email. This session can come from being passed in the default constructor, or made by <code>Mailer</code> 080 * directly, when no <code>Session</code> instance was provided. 081 * 082 * @see #Mailer(Session) 083 * @see #Mailer(String, Integer, String, String, TransportStrategy) 084 */ 085 private final Session session; 086 087 /** 088 * The transport protocol strategy enum that actually handles the session configuration. Session configuration meaning setting the right 089 * properties for the appropriate transport type (ie. <em>"mail.smtp.host"</em> for SMTP, <em>"mail.smtps.host"</em> for SMTPS). 090 */ 091 private TransportStrategy transportStrategy; 092 093 /** 094 * Email address restriction flags set either by constructor or overridden by getter by user. 095 * 096 * @see EmailAddressValidationCriteria 097 */ 098 private EmailAddressValidationCriteria emailAddressValidationCriteria; 099 100 /** 101 * Default constructor, stores the given mail session for later use. Assumes that *all* properties used to make a connection are 102 * configured (host, port, authentication and transport protocol settings). 103 * <p> 104 * Also leaves email address validation criteria empty so that no validation is being performed. Validation errors will come from the 105 * smtp server instead. 106 * 107 * @param session A preconfigured mail {@link Session} object with which a {@link Message} can be produced. 108 */ 109 public Mailer(final Session session) { 110 this.session = session; 111 this.emailAddressValidationCriteria = null; 112 } 113 114 /** 115 * Overloaded constructor which produces a new {@link Session} on the fly. Use this if you don't have a mail session configured in your 116 * web container, or Spring context etc. 117 * <p> 118 * Also leaves email address validation criteria empty so that no validation is being performed. Validation errors will come from the 119 * smtp server instead. 120 * 121 * @param host The address URL of the SMTP server to be used. 122 * @param port The port of the SMTP server. 123 * @param username An optional username, may be <code>null</code>. 124 * @param password An optional password, may be <code>null</code>, but only if username is <code>null</code> as well. 125 * @param transportStrategy The transport protocol configuration type for handling SSL or TLS (or vanilla SMTP) 126 */ 127 public Mailer(final String host, final Integer port, final String username, final String password, 128 final TransportStrategy transportStrategy) { 129 // we're doing these validations manually instead of using Apache Commons to avoid another dependency 130 if (host == null || host.trim().equals("")) { 131 throw new MailException(MailException.MISSING_HOST); 132 } else if ((password != null && !password.trim().equals("")) && (username == null || username.trim().equals(""))) { 133 throw new MailException(MailException.MISSING_USERNAME); 134 } 135 this.transportStrategy = transportStrategy; 136 this.session = createMailSession(host, port, username, password); 137 this.emailAddressValidationCriteria = null; 138 } 139 140 /** 141 * Actually instantiates and configures the {@link Session} instance. Delegates resolving transport protocol specific properties to the 142 * {@link #transportStrategy} in two ways: 143 * <ol> 144 * <li>request an initial property list which the strategy may pre-populate</li> 145 * <li>by requesting the property names according to the respective transport protocol it handles (for the host property for example it 146 * would be <em>"mail.smtp.host"</em> for SMTP and <em>"mail.smtps.host"</em> for SMTPS)</li> 147 * </ol> 148 * 149 * @param host The address URL of the SMTP server to be used. 150 * @param port The port of the SMTP server. 151 * @param username An optional username, may be <code>null</code>. 152 * @param password An optional password, may be <code>null</code>. 153 * @return A fully configured <code>Session</code> instance complete with transport protocol settings. 154 * @see TransportStrategy#generateProperties() 155 * @see TransportStrategy#propertyNameHost() 156 * @see TransportStrategy#propertyNamePort() 157 * @see TransportStrategy#propertyNameUsername() 158 * @see TransportStrategy#propertyNameAuthenticate() 159 */ 160 protected Session createMailSession(final String host, final Integer port, final String username, final String password) { 161 if (transportStrategy == null) { 162 logger.warn("Transport Strategy not set, using plain SMTP strategy instead!"); 163 transportStrategy = TransportStrategy.SMTP_PLAIN; 164 } 165 Properties props = transportStrategy.generateProperties(); 166 props.put(transportStrategy.propertyNameHost(), host); 167 if (port != null) { 168 props.put(transportStrategy.propertyNamePort(), String.valueOf(port)); 169 } else { 170 // let JavaMail's Transport objects determine deault port base don the used protocol 171 } 172 173 if (username != null) { 174 props.put(transportStrategy.propertyNameUsername(), username); 175 } 176 177 if (password != null) { 178 props.put(transportStrategy.propertyNameAuthenticate(), "true"); 179 return Session.getInstance(props, new Authenticator() { 180 @Override 181 protected PasswordAuthentication getPasswordAuthentication() { 182 return new PasswordAuthentication(username, password); 183 } 184 }); 185 } else { 186 return Session.getInstance(props); 187 } 188 } 189 190 /** 191 * Overloaded constructor which produces a new {@link Session} on the fly, using default vanilla SMTP transport protocol. 192 * 193 * @param host The address URL of the SMTP server to be used. 194 * @param port The port of the SMTP server. 195 * @param username An optional username, may be <code>null</code>. 196 * @param password An optional password, may be <code>null</code>, but only if username is <code>null</code> as well. 197 * @see #Mailer(String, Integer, String, String, TransportStrategy) 198 */ 199 public Mailer(final String host, final Integer port, final String username, final String password) { 200 this(host, port, username, password, TransportStrategy.SMTP_PLAIN); 201 } 202 203 /** 204 * In case Simple Java Mail falls short somehow, you can get a hold of the internal {@link Session} instance to debug or tweak. Please 205 * let us know why you are needing this on https://github.com/bbottema/simple-java-mail/issues. 206 */ 207 public Session getSession() { 208 logger.warn("Providing access to Session instance for emergency fall-back scenario. Please let us know why you need it."); 209 logger.warn("\t>https://github.com/bbottema/simple-java-mail/issues"); 210 return session; 211 } 212 213 /** 214 * Actually sets {@link Session#setDebug(boolean)} so that it generates debug information. 215 * 216 * @param debug Flag to indicate debug mode yes/no. 217 */ 218 public void setDebug(final boolean debug) { 219 session.setDebug(debug); 220 } 221 222 /** 223 * Copies all property entries into the {@link Session} using {@link Session#getProperties()}. 224 * 225 * @param properties The source properties to add or override in the internal {@link Session} instance. 226 */ 227 public void applyProperties(final Properties properties) { 228 session.getProperties().putAll(properties); 229 } 230 231 /** 232 * Processes an {@link Email} instance into a completely configured {@link Message}. 233 * <p> 234 * Sends the Sun JavaMail {@link Message} object using {@link Session#getTransport()}. It will call {@link Transport#connect()} assuming 235 * all connection details have been configured in the provided {@link Session} instance. 236 * <p> 237 * Performs a call to {@link Message#saveChanges()} as the Sun JavaMail API indicates it is needed to configure the message headers and 238 * providing a message id. 239 * 240 * @param email The information for the email to be sent. 241 * @throws MailException Can be thrown if an email isn't validating correctly, or some other problem occurs during connection, sending 242 * etc. 243 * @see #validate(Email) 244 * @see #prepareMessage(Email, MimeEmailMessageWrapper) 245 * @see #setRecipients(Email, Message) 246 * @see #setTexts(Email, MimeMultipart) 247 * @see #setEmbeddedImages(Email, MimeMultipart) 248 * @see #setAttachments(Email, MimeMultipart) 249 */ 250 public final void sendMail(final Email email) 251 throws MailException { 252 if (validate(email)) { 253 try { 254 // create new wrapper for each mail being sent (enable sending multiple emails with one mailer) 255 final MimeEmailMessageWrapper messageRoot = new MimeEmailMessageWrapper(); 256 // fill and send wrapped mime message parts 257 final Message message = prepareMessage(email, messageRoot); 258 logSession(session, transportStrategy); 259 message.saveChanges(); // some headers and id's will be set for this specific message 260 Transport transport = session.getTransport(); 261 transport.connect(); 262 transport.sendMessage(message, message.getAllRecipients()); 263 transport.close(); 264 } catch (final UnsupportedEncodingException e) { 265 logger.error(e.getMessage(), e); 266 throw new MailException(String.format(MailException.INVALID_ENCODING, e.getMessage())); 267 } catch (final MessagingException e) { 268 logger.error(e.getMessage(), e); 269 throw new MailException(String.format(MailException.GENERIC_ERROR, e.getMessage()), e); 270 } 271 } 272 } 273 274 /** 275 * Simply logs host details, credentials used and whether authentication will take place and finally the transport protocol used. 276 */ 277 private void logSession(Session session, TransportStrategy transportStrategy) { 278 Properties properties = session.getProperties(); 279 final String specifics; 280 if (transportStrategy != null) { 281 final String logmsg = "starting mail session (host: %s, port: %s, username: %s, authenticate: %s, transport: %s)"; 282 specifics = String.format(logmsg, properties.get(transportStrategy.propertyNameHost()), 283 properties.get(transportStrategy.propertyNamePort()), properties.get(transportStrategy.propertyNameUsername()), 284 properties.get(transportStrategy.propertyNameAuthenticate()), transportStrategy); 285 } else { 286 specifics = properties.toString(); 287 } 288 logger.debug(String.format("starting mail session (%s)", specifics)); 289 } 290 291 /** 292 * Validates an {@link Email} instance. Validation fails if the subject is missing, content is missing, or no recipients are defined. 293 * 294 * @param email The email that needs to be configured correctly. 295 * @return Always <code>true</code> (throws a {@link MailException} exception if validation fails). 296 * @throws MailException Is being thrown in any of the above causes. 297 * @see EmailValidationUtil 298 */ 299 public boolean validate(final Email email) 300 throws MailException { 301 if (email.getText() == null && email.getTextHTML() == null) { 302 throw new MailException(MailException.MISSING_CONTENT); 303 } else if (email.getSubject() == null || email.getSubject().equals("")) { 304 throw new MailException(MailException.MISSING_SUBJECT); 305 } else if (email.getRecipients().size() == 0) { 306 throw new MailException(MailException.MISSING_RECIPIENT); 307 } else if (email.getFromRecipient() == null) { 308 throw new MailException(MailException.MISSING_SENDER); 309 } else if (emailAddressValidationCriteria != null) { 310 if (!EmailValidationUtil.isValid(email.getFromRecipient().getAddress(), emailAddressValidationCriteria)) { 311 throw new MailException(String.format(MailException.INVALID_SENDER, email)); 312 } 313 for (final Recipient recipient : email.getRecipients()) { 314 if (!EmailValidationUtil.isValid(recipient.getAddress(), emailAddressValidationCriteria)) { 315 throw new MailException(String.format(MailException.INVALID_RECIPIENT, email)); 316 } 317 } 318 if (email.getReplyToRecipient() != null) { 319 if (!EmailValidationUtil.isValid(email.getReplyToRecipient().getAddress(), emailAddressValidationCriteria)) { 320 throw new MailException(String.format(MailException.INVALID_REPLYTO, email)); 321 } 322 } 323 } 324 return true; 325 } 326 327 /** 328 * Creates a new {@link MimeMessage} instance and prepares it in the email structure, so that it can be filled and send. 329 * <p> 330 * Fills subject, from,reply-to, content, sent-date, recipients, texts, embedded images, attachments, content and adds all headers. 331 * 332 * @param email The email message from which the subject and From-address are extracted. 333 * @param messageRoot The root of the email which holds everything (filled with some email data). 334 * @return A fully preparated {@link Message} instance, ready to be sent. 335 * @throws MessagingException May be thrown when the message couldn't be processed by JavaMail. 336 * @throws UnsupportedEncodingException Zie {@link InternetAddress#InternetAddress(String, String)}. 337 */ 338 private Message prepareMessage(final Email email, final MimeEmailMessageWrapper messageRoot) 339 throws MessagingException, UnsupportedEncodingException { 340 final MimeMessage message = new MimeMessage(session); 341 // set basic email properties 342 message.setSubject(email.getSubject(), CHARACTER_ENCODING); 343 message.setFrom(new InternetAddress(email.getFromRecipient().getAddress(), email.getFromRecipient().getName(), CHARACTER_ENCODING)); 344 setReplyTo(email, message); 345 setRecipients(email, message); 346 // fill multipart structure 347 setTexts(email, messageRoot.multipartAlternativeMessages); 348 setEmbeddedImages(email, messageRoot.multipartRelated); 349 setAttachments(email, messageRoot.multipartRoot); 350 message.setContent(messageRoot.multipartRoot); 351 setHeaders(email, message); 352 message.setSentDate(new Date()); 353 return message; 354 } 355 356 /** 357 * Fills the {@link Message} instance with recipients from the {@link Email}. 358 * 359 * @param email The message in which the recipients are defined. 360 * @param message The javax message that needs to be filled with recipients. 361 * @throws UnsupportedEncodingException See {@link InternetAddress#InternetAddress(String, String)}. 362 * @throws MessagingException See {@link Message#addRecipient(javax.mail.Message.RecipientType, Address)} 363 */ 364 private void setRecipients(final Email email, final Message message) 365 throws UnsupportedEncodingException, MessagingException { 366 for (final Recipient recipient : email.getRecipients()) { 367 final Address address = new InternetAddress(recipient.getAddress(), recipient.getName(), CHARACTER_ENCODING); 368 message.addRecipient(recipient.getType(), address); 369 } 370 } 371 372 /** 373 * Fills the {@link Message} instance with reply-to address. 374 * 375 * @param email The message in which the recipients are defined. 376 * @param message The javax message that needs to be filled with reply-to address. 377 * @throws UnsupportedEncodingException See {@link InternetAddress#InternetAddress(String, String)}. 378 * @throws MessagingException See {@link Message#setReplyTo(Address[])} 379 */ 380 private void setReplyTo(final Email email, final Message message) 381 throws UnsupportedEncodingException, MessagingException { 382 final Recipient replyToRecipient = email.getReplyToRecipient(); 383 if (replyToRecipient != null) { 384 InternetAddress replyToAddress = new InternetAddress(replyToRecipient.getAddress(), replyToRecipient.getName(), 385 CHARACTER_ENCODING); 386 message.setReplyTo(new Address[] { replyToAddress }); 387 } 388 } 389 390 /** 391 * Fills the {@link Message} instance with the content bodies (text and html). 392 * 393 * @param email The message in which the content is defined. 394 * @param multipartAlternativeMessages See {@link MimeMultipart#addBodyPart(BodyPart)} 395 * @throws MessagingException See {@link BodyPart#setText(String)}, {@link BodyPart#setContent(Object, String)} and 396 * {@link MimeMultipart#addBodyPart(BodyPart)}. 397 */ 398 private void setTexts(final Email email, final MimeMultipart multipartAlternativeMessages) 399 throws MessagingException { 400 if (email.getText() != null) { 401 final MimeBodyPart messagePart = new MimeBodyPart(); 402 messagePart.setText(email.getText(), CHARACTER_ENCODING); 403 multipartAlternativeMessages.addBodyPart(messagePart); 404 } 405 if (email.getTextHTML() != null) { 406 final MimeBodyPart messagePartHTML = new MimeBodyPart(); 407 messagePartHTML.setContent(email.getTextHTML(), "text/html; charset=\"" + CHARACTER_ENCODING + "\""); 408 multipartAlternativeMessages.addBodyPart(messagePartHTML); 409 } 410 } 411 412 /** 413 * Fills the {@link Message} instance with the embedded images from the {@link Email}. 414 * 415 * @param email The message in which the embedded images are defined. 416 * @param multipartRelated The branch in the email structure in which we'll stuff the embedded images. 417 * @throws MessagingException See {@link MimeMultipart#addBodyPart(BodyPart)} and 418 * {@link #getBodyPartFromDatasource(AttachmentResource, String)} 419 */ 420 private void setEmbeddedImages(final Email email, final MimeMultipart multipartRelated) 421 throws MessagingException { 422 for (final AttachmentResource embeddedImage : email.getEmbeddedImages()) { 423 multipartRelated.addBodyPart(getBodyPartFromDatasource(embeddedImage, Part.INLINE)); 424 } 425 } 426 427 /** 428 * Fills the {@link Message} instance with the attachments from the {@link Email}. 429 * 430 * @param email The message in which the attachments are defined. 431 * @param multipartRoot The branch in the email structure in which we'll stuff the attachments. 432 * @throws MessagingException See {@link MimeMultipart#addBodyPart(BodyPart)} and 433 * {@link #getBodyPartFromDatasource(AttachmentResource, String)} 434 */ 435 private void setAttachments(final Email email, final MimeMultipart multipartRoot) 436 throws MessagingException { 437 for (final AttachmentResource resource : email.getAttachments()) { 438 multipartRoot.addBodyPart(getBodyPartFromDatasource(resource, Part.ATTACHMENT)); 439 } 440 } 441 442 /** 443 * Sets all headers on the {@link Message} instance. Since we're not using a high-level JavaMail method, the JavaMail library says we 444 * need to do some encoding and 'folding' manually, to get the value right for the headers (see {@link MimeUtility}. 445 * 446 * @param email The message in which the headers are defined. 447 * @param message The {@link Message} on which to set the raw, encoded and folded headers. 448 * @throws UnsupportedEncodingException See {@link MimeUtility#encodeText(String, String, String)} 449 * @throws MessagingException See {@link Message#addHeader(String, String)} 450 * @see {@link MimeUtility#encodeText(String, String, String)} 451 * @see MimeUtility#fold(int, String) 452 */ 453 private void setHeaders(final Email email, final Message message) 454 throws UnsupportedEncodingException, MessagingException { 455 // add headers (for raw message headers we need to 'fold' them using MimeUtility 456 for (Map.Entry<String, String> header : email.getHeaders().entrySet()) { 457 String headerName = header.getKey(); 458 String headerValue = MimeUtility.encodeText(header.getValue(), CHARACTER_ENCODING, null); 459 String foldedHeaderValue = MimeUtility.fold(headerName.length() + 2, headerValue); 460 message.addHeader(header.getKey(), foldedHeaderValue); 461 } 462 } 463 464 /** 465 * Helper method which generates a {@link BodyPart} from an {@link AttachmentResource} (from its {@link DataSource}) and a disposition 466 * type ({@link Part#INLINE} or {@link Part#ATTACHMENT}). With this the attachment data can be converted into objects that fit in the 467 * email structure. <br> 468 * <br> 469 * For every attachment and embedded image a header needs to be set. 470 * 471 * @param resource An object that describes the attachment and contains the actual content data. 472 * @param dispositionType The type of attachment, {@link Part#INLINE} or {@link Part#ATTACHMENT} . 473 * @return An object with the attachment data read for placement in the email structure. 474 * @throws MessagingException All BodyPart setters. 475 */ 476 private BodyPart getBodyPartFromDatasource(final AttachmentResource resource, final String dispositionType) 477 throws MessagingException { 478 final BodyPart attachmentPart = new MimeBodyPart(); 479 final DataSource ds = resource.getDataSource(); 480 // setting headers isn't working nicely using the javax mail API, so let's do that manually 481 attachmentPart.setDataHandler(new DataHandler(resource.getDataSource())); 482 attachmentPart.setFileName(resource.getName()); 483 attachmentPart.setHeader("Content-Type", ds.getContentType() + "; filename=" + ds.getName() + "; name=" + ds.getName()); 484 attachmentPart.setHeader("Content-ID", String.format("<%s>", ds.getName())); 485 attachmentPart.setDisposition(dispositionType + "; size=0"); 486 return attachmentPart; 487 } 488 489 /** 490 * This class conveniently wraps all necessary mimemessage parts that need to be filled with content, attachments etc. The root is 491 * ultimately sent using JavaMail.<br> 492 * <br> 493 * The constructor creates a new email message constructed from {@link MimeMultipart} as follows: 494 * 495 * <pre> 496 * - root 497 * - related 498 * - alternative 499 * - mail tekst 500 * - mail html tekst 501 * - embedded images 502 * - attachments 503 * </pre> 504 * 505 * @author Benny Bottema 506 */ 507 private class MimeEmailMessageWrapper { 508 509 private final MimeMultipart multipartRoot; 510 511 private final MimeMultipart multipartRelated; 512 513 private final MimeMultipart multipartAlternativeMessages; 514 515 /** 516 * Creates an email skeleton structure, so that embedded images, attachments and (html) texts are being processed properly. 517 */ 518 MimeEmailMessageWrapper() { 519 multipartRoot = new MimeMultipart("mixed"); 520 final MimeBodyPart contentRelated = new MimeBodyPart(); 521 multipartRelated = new MimeMultipart("related"); 522 final MimeBodyPart contentAlternativeMessages = new MimeBodyPart(); 523 multipartAlternativeMessages = new MimeMultipart("alternative"); 524 try { 525 // construct mail structure 526 multipartRoot.addBodyPart(contentRelated); 527 contentRelated.setContent(multipartRelated); 528 multipartRelated.addBodyPart(contentAlternativeMessages); 529 contentAlternativeMessages.setContent(multipartAlternativeMessages); 530 } catch (final MessagingException e) { 531 logger.error(e.getMessage(), e); 532 throw new RuntimeException(e.getMessage(), e); 533 } 534 } 535 } 536 537 /** 538 * Overrides the default email address validation restrictions when validating and sending emails using the current <code>Mailer</code> 539 * instance. By default no validation will be performed by simple-java-mail, until a criteria object has been set. 540 * 541 * @param emailAddressValidationCriteria Refer to 542 * {@link EmailAddressValidationCriteria#EmailAddressValidationCriteria(boolean, boolean)}. 543 */ 544 public void setEmailAddressValidationCriteria(EmailAddressValidationCriteria emailAddressValidationCriteria) { 545 this.emailAddressValidationCriteria = emailAddressValidationCriteria; 546 } 547}