001package com.randomnoun.common.email; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006import java.io.IOException; 007import java.io.InputStream; 008import java.util.ArrayList; 009import java.util.Date; 010import java.util.Iterator; 011import java.util.List; 012import java.util.Map; 013import java.util.Properties; 014 015import org.apache.log4j.Logger; 016 017import com.randomnoun.common.StreamUtil; 018 019import jakarta.activation.DataHandler; 020import jakarta.activation.DataSource; 021import jakarta.activation.FileDataSource; 022import jakarta.mail.Message; 023import jakarta.mail.MessagingException; 024import jakarta.mail.Part; 025import jakarta.mail.Session; 026import jakarta.mail.Transport; 027import jakarta.mail.internet.InternetAddress; 028import jakarta.mail.internet.MimeBodyPart; 029import jakarta.mail.internet.MimeMessage; 030import jakarta.mail.internet.MimeMultipart; 031 032/** 033 * Provides a simple, one-class wrapper around the Java Mail API. Use the 034 * {@link #emailTo(String, String, String, String, String, String, String)} 035 * method to send a mail in a single method call, or 036 * {@link #emailToNoEx(String, String, String, String, String, String, String)} 037 * to send an email ignoring exceptions. 038 * 039 * <p>SMTP authentication is supported. 040 * 041 * <p>S/MIME encryption isn't supported, but would be relatively easy to add; 042 * 043 * <p>Use the (somewhat more complex) {@link #emailAttachmentTo(Map)} method to send emails 044 * with attachments sourced from the file system, byte arrays or the classpath, 045 * or to modify the email headers. 046 * 047 * @author knoxg 048 */ 049public class EmailWrapper { 050 051 public static Logger logger = Logger.getLogger(EmailWrapper.class); 052 053 private static boolean isBlank(String string) { 054 return (string==null || "".equals(string)); 055 } 056 057 /** 058 * A static method which provides the facility to easily send off an 059 * email using the JavaMail API. Mail-generated exceptions are sent to 060 * logger.error. 061 * 062 * @param to A comma-separated list of recipients 063 * @param from The address to place in the From: field of the email 064 * @param subject The subject text 065 * @param bodyText The message text 066 */ 067 public static void emailToNoEx(String to, String from, String host, String subject, 068 String bodyText, String username, String password) { 069 try { 070 emailTo(to, from, host, subject, bodyText, username, password); 071 } catch (MessagingException me) { 072 logger.error("A messaging exception occurred whilst sending an email", me); 073 } 074 } 075 076 /** 077 * A simpler version of emailTo. 078 * 079 * @param to A comma-separated list of recipients 080 * @param from The address to place in the From: field of the email 081 * @param host The SMTP host to use 082 * @param subject The subject text 083 * @param bodyText The message text 084 * @param username The SMTP user to send from (null if not authenticated) 085 * @param password The SMTP password to use (null if not authenticated) 086 * 087 */ 088 public static void emailTo(String to, String from, String host, String subject, String bodyText, 089 String username, String password) 090 throws MessagingException { 091 Properties props; 092 093 props = new Properties(); 094 props.put("to", to); 095 props.put("from", from); 096 props.put("host", host); 097 props.put("subject", subject); 098 props.put("bodyText", bodyText); 099 if (username!=null) { props.put("username", username); } 100 if (password!=null) { props.put("password", password); } 101 emailAttachmentTo(props); 102 } 103 104 /** 105 * A simpler version of emailTo, with HTML. 106 * 107 * @param to A comma-separated list of recipients 108 * @param from The address to place in the From: field of the email 109 * @param host The SMTP host to use 110 * @param subject The subject text 111 * @param bodyText The message text 112 * @param bodyHtml The message text, in HTML format 113 * @param username The SMTP user to send from (null if not authenticated) 114 * @param password The SMTP password to use (null if not authenticated) 115 * 116 */ 117 public static void emailTo(String to, String from, String host, String subject, String bodyText, 118 String bodyHtml, String username, String password) 119 throws MessagingException { 120 Properties props; 121 122 props = new Properties(); 123 props.put("to", to); 124 props.put("from", from); 125 props.put("host", host); 126 props.put("subject", subject); 127 props.put("bodyText", bodyText); 128 props.put("bodyHtml", bodyHtml); 129 130 if (username!=null) { props.put("username", username); } 131 if (password!=null) { props.put("password", password); } 132 emailAttachmentTo(props); 133 } 134 135 136 /** Return an array of InternetAddress objects from a comma-separated list, 137 * and a header list. The headerList object passed to the 138 * {@link #emailAttachmentTo(java.util.Map)} object is passed into this 139 * object; if a header exists in this list with the name _headerName_ passed 140 * to this method, then the email addresses contained within it will also 141 * be appended to the returned array. 142 * 143 * @param stringList A comma-separated list of names which will be appended 144 * to the array 145 * @param headerName The header name that will be used to search for 146 * more email addresses 147 * @param headerList A structured list, describing a list of custom 148 * headers to be added to an email, as passed to emailAttachmentTo() 149 * 150 * @return an InternetAddress[] structure, suitable for use in various 151 * JavaMail API calls 152 */ 153 private static InternetAddress[] getAddressList(String stringList, String headerName, List<Map<String, Object>> headerList) 154 throws MessagingException { 155 String[] addresses; 156 List<InternetAddress> addressList = new ArrayList<>(); 157 int i; 158 159 // add addresses in 'stringList' to addressList array 160 if (!isBlank(stringList)) { 161 addresses = stringList.split(","); 162 163 for (i = 0; i < addresses.length; i++) { 164 addressList.add(new InternetAddress(addresses[i])); 165 } 166 } 167 168 // do the same for any headers matching the headerName supplied 169 if (!isBlank(headerName) && (headerList != null)) { 170 for (Iterator<Map<String, Object>> j = headerList.iterator(); j.hasNext();) { 171 Map<String, Object> map = (Map<String, Object>) j.next(); 172 173 if (headerName.equals(map.get("name"))) { 174 addresses = ((String) map.get("value")).split(","); 175 176 for (i = 0; i < addresses.length; i++) { 177 addressList.add(new InternetAddress(addresses[i])); 178 } 179 } 180 } 181 } 182 183 return (InternetAddress[]) addressList.toArray(new InternetAddress[] { }); 184 } 185 186 /** Versatile extendable email wrapper method. 187 * 188 * This method takes a structured map as input, representing the 189 * message to be sent. It supports multiple to, from, cc, bcc, and replyto 190 * fields. It supports custom headers. It supports attachments from 191 * files on the filesystem, classpath resources, and passed in directly. 192 * 193 * The following attributes are accepted by this method 194 * 195 * <ul> 196 * <li>to - To addresses, comma-separated. Will also include any 197 * custom headers supplied with the headername of 'to' 198 * <li>from - From address 199 * <li>subject - The subject of the email 200 * <li>bodyText - The body text of the email 201 * <li>bodyHtml - The body text of the email, in HTML format 202 * <li>username - If not null, the username to authenticate to the SMTP server 203 * <li>password - If not null, the password with which to authenticate to the SMTP 204 * <li>cc - CC addresses, comma-separated. Will also include any 205 * custom headers supplied with the headername of 'cc' 206 * <li>bcc - BCC addresses, comma-separated. Will also include any 207 * custom headers supplied with the headername of 'bcc' 208 * <li>replyTo - Reply-To addresses, comma-separated. Will also include any 209 * custom headers supplied with the headername of 'replyTo' 210 * <li>client - email client to use in generated MessageID (defaults to "JavaMail") 211 * <li>suffix - suffix to use in generated MessageID (defaults to "username@host") 212 * (both client & suffix must be specified together) 213 * <li>sessionProperties - A map containing additional JavaMail session properties 214 * <li>headers - A structured list of custom headers 215 * <ul> 216 * <li>name - The header name 217 * <li>value - The header value 218 * </ul> 219 * 220 * <li>attachFiles Attachments to this email, sourced from the filesystem 221 * <ul> 222 * <li>filename - The file on the local filesystem which contains the data to send 223 * <li>attachFilename - The name of the file visible in the email 224 * <li>contentType - The content-type to assign to this file. Will default to 225 * <code>application/octet-stream</code> 226 * </ul> 227 * 228 * <li>attachResources Attachments to this email, sourced from the classLoader 229 * <ul> 230 * <li>resource - The resource name 231 * <li>attachFilename - The name of the file visible in the email 232 * <li>contentType - The content-type to assign to this file. Will default to 233 * <code>application/octet-stream</code> 234 * <li>classLoader - Any class which will indicate which class loader to 235 * use to find this resource, or a ClassLoader instance. 236 * If missing, will default to the class loader of the 237 * EmailWrapper class. 238 * </ul> 239 * 240 * <li>attachData Attachments to this email, passed in directly 241 * <ul> 242 * <li>data - The data comprising the attachment. Can be either a 243 * byte array, or a string. If the object is of any other 244 * type, then it is converted to a string using it's 245 * .toString() method. 246 * <li>attachFilename - The name of the file visible in the email 247 * <li>contentType - The content-type to assign to this file. Will default 248 * to <code>application/octet-stream</code> 249 * </ul> 250 * </ul> 251 * 252 * This method returns no values 253 * 254 * @throws MessagingException An error occurred sending the email 255 */ 256 @SuppressWarnings("unchecked") 257 public static void emailAttachmentTo(Map<Object, Object> params) // Map<Object, Object> so it can accept Properties objects 258 throws MessagingException { 259 logger.debug("Inside emailAttachmentTo method with params"); 260 261 // get parameters out of map 262 String to = (String) params.get("to"); 263 String from = (String) params.get("from"); 264 String cc = (String) params.get("cc"); 265 String bcc = (String) params.get("bcc"); 266 String replyTo = (String) params.get("replyTo"); 267 String bodyText = (String) params.get("bodyText"); 268 String bodyHtml = (String) params.get("bodyHtml"); 269 String subject = (String) params.get("subject"); 270 String host = (String) params.get("host"); 271 String username = (String) params.get("username"); 272 String password = (String) params.get("password"); 273 String client = (String) params.get("client"); 274 String suffix = (String) params.get("suffix"); 275 276 List<Map<String, Object>> attachFiles = (List<Map<String, Object>>) params.get("attachFiles"); // list of filenames to retrieve from disk 277 List<Map<String, Object>> attachResources = (List<Map<String, Object>>) params.get("attachResources"); // list of resources to retrieve from classpath 278 List<Map<String, Object>> attachData = (List<Map<String, Object>>) params.get("attachData"); // list of attachment data 279 List<Map<String, Object>> headers = (List<Map<String, Object>>) params.get("headers"); 280 Map<String, Object> sessionProperties = (Map<String, Object>) params.get("sessionProperties"); 281 282 boolean isMultipart = false; 283 boolean isAltContent = false; // true if both text and html 284 285 // validate / set defaults 286 if (isBlank(to)) { throw new IllegalArgumentException("Empty 'to' address"); } 287 if (isBlank(host)) { host = "127.0.0.1"; } 288 289 // set up mail session 290 Properties props = new Properties(); 291 props.put("mail.smtp.host", host); 292 if (username != null) { props.put("mail.smtp.user", username); } 293 if (sessionProperties != null) { 294 props.putAll(sessionProperties); 295 } 296 297 Session session = Session.getInstance(props, null); 298 Message msg; 299 if (isBlank(client) || isBlank(suffix)) { 300 msg = new MimeMessage(session); 301 } else { 302 msg = new CustomMimeMessage(session, client, suffix); 303 } 304 305 if (!isBlank(from)) { 306 msg.setFrom(new InternetAddress(from)); 307 } 308 if (!isBlank(subject)) { 309 msg.setSubject(subject); 310 } 311 312 msg.setSentDate(new Date()); 313 314 if (bodyHtml!=null && bodyText!=null) { 315 isMultipart = true; 316 isAltContent = true; 317 } 318 319 // add receipient information 320 InternetAddress[] toArray = getAddressList(to, "to", headers); 321 if (toArray.length > 0) { msg.setRecipients(Message.RecipientType.TO, toArray); } 322 323 InternetAddress[] ccArray = getAddressList(cc, "cc", headers); 324 if (ccArray.length > 0) { msg.setRecipients(Message.RecipientType.CC, ccArray); } 325 326 InternetAddress[] bccArray = getAddressList(bcc, "bcc", headers); 327 if (bccArray.length > 0) { msg.setRecipients(Message.RecipientType.BCC, bccArray); } 328 329 InternetAddress[] replyToArray = getAddressList(replyTo, "replyTo", headers); 330 if (replyToArray.length > 0) { msg.setReplyTo(replyToArray); } 331 332 // add other headers 333 if (headers != null) { 334 for (Iterator<Map<String, Object>> i = headers.iterator(); i.hasNext();) { 335 Map<String, Object> map = i.next(); 336 String headerName = (String) map.get("name"); 337 338 if (headerName != null && !headerName.equals("to") && !headerName.equals("cc") && !headerName.equals("bcc")) { 339 msg.addHeader(headerName, (String) map.get("value")); 340 } 341 } 342 } 343 344 // create multipart message. Beware of non us-ascii charsets. 345 MimeMultipart multiPart; 346 if (isAltContent) { 347 multiPart = new MimeMultipart("alternative"); 348 MimeBodyPart bodyPart = new MimeBodyPart(); 349 bodyPart.setText(bodyText /*, "us-ascii", "plain" */); 350 multiPart.addBodyPart(bodyPart); 351 bodyPart = new MimeBodyPart(); 352 bodyPart.setText(bodyHtml, "us-ascii", "html" ); /* creates text/html Content-type */ 353 multiPart.addBodyPart(bodyPart); 354 } else { 355 multiPart = new MimeMultipart("mixed"); 356 MimeBodyPart bodyPart = new MimeBodyPart(); 357 if (bodyText!=null) { 358 bodyPart.setText(bodyText /*, "us-ascii", "text/plain" */); 359 } else if (bodyHtml!=null) { 360 bodyPart.setText(bodyHtml, "us-ascii", "html"); 361 } 362 multiPart.addBodyPart(bodyPart); 363 } 364 365 // create attachments from files on disk 366 if (attachFiles != null) { 367 for (Iterator<Map<String, Object>> i = attachFiles.iterator(); i.hasNext();) { 368 Map<String, Object> map = i.next(); 369 String filename = (String) map.get("filename"); 370 String attachFilename = (String) map.get("attachFilename"); 371 DataSource dataSource = new FileDataSource(filename); 372 MimeBodyPart attachment = new MimeBodyPart(); 373 374 attachment.setDataHandler(new DataHandler(dataSource)); 375 attachment.setFileName(attachFilename); //attachFile); 376 multiPart.addBodyPart(attachment); 377 isMultipart = true; 378 } 379 } 380 381 // create attachments from a list of classpath resources 382 try { 383 if (attachResources != null) { 384 for (Iterator<Map<String, Object>> i = attachResources.iterator(); i.hasNext();) { 385 Map<String, Object> map = i.next(); 386 String resource = (String) map.get("resource"); 387 String attachFilename = (String) map.get("attachFilename"); 388 Object classLoaderObject = (Object) map.get("classloader"); 389 String contentType = (String) map.get("contentType"); 390 391 if (isBlank(contentType)) { 392 contentType = "application/octet-stream"; 393 } 394 395 ClassLoader classLoader; 396 397 if (classLoaderObject == null) { 398 classLoaderObject = EmailWrapper.class; 399 } 400 401 if (classLoaderObject instanceof Class) { 402 classLoader = ((Class<?>) classLoaderObject).getClassLoader(); 403 } else if (classLoaderObject instanceof ClassLoader) { 404 classLoader = (ClassLoader) classLoaderObject; 405 } else { 406 classLoader = classLoaderObject.getClass().getClassLoader(); 407 } 408 409 InputStream inputStream = classLoader.getResourceAsStream(resource); 410 byte[] attachmentData = StreamUtil.getByteArray(inputStream); 411 DataSource dataSource = new ByteArrayDataSource(attachmentData, attachFilename, contentType); 412 MimeBodyPart attachment = new MimeBodyPart(); 413 414 attachment.setDataHandler(new DataHandler(dataSource)); 415 attachment.setFileName(attachFilename); 416 multiPart.addBodyPart(attachment); 417 isMultipart = true; 418 } 419 } 420 } catch (IOException ioe) { 421 throw new MessagingException("Error reading resource", ioe); 422 } 423 424 // create attachments from data passed in to this method 425 if (attachData != null) { 426 for (Iterator<Map<String, Object>> i = attachData.iterator(); i.hasNext();) { 427 Map<String, Object> map = i.next(); 428 String attachFilename = (String) map.get("attachFilename"); 429 Object data = map.get("data"); 430 String contentType = (String) map.get("contentType"); 431 String contentId = (String) map.get("contentId"); 432 String disposition = (String) map.get("disposition"); 433 if (isBlank(contentType)) { 434 contentType = "application/octet-stream"; 435 } 436 437 438 byte[] attachmentData; 439 440 if (data instanceof byte[]) { 441 attachmentData = (byte[]) data; 442 } else if (data instanceof String) { 443 attachmentData = ((String) data).getBytes(); 444 } else { 445 attachmentData = data.toString().getBytes(); 446 } 447 448 DataSource dataSource = new ByteArrayDataSource(attachmentData, attachFilename, contentType); 449 MimeBodyPart attachment = new MimeBodyPart(); 450 451 attachment.setDataHandler(new DataHandler(dataSource)); 452 attachment.setFileName(attachFilename); 453 multiPart.addBodyPart(attachment); 454 if (contentId!=null) { 455 attachment.addHeader("Content-ID", contentId); 456 } 457 if ("inline".equals(disposition)) { 458 attachment.setDisposition(Part.INLINE); 459 } else { 460 attachment.setDisposition(Part.ATTACHMENT); 461 } 462 463 464 isMultipart = true; 465 } 466 } 467 468 // only make this a multi-part message if we have attachments 469 if (isMultipart) { 470 471 msg.setContent(multiPart); 472 } else { 473 if (bodyText!=null) { 474 msg.setText(bodyText); 475 } else if (bodyHtml!=null) { 476 throw new UnsupportedOperationException("HTML text supplied without plain text. Test this before using."); 477 } 478 } 479 480 Transport tr = session.getTransport("smtp"); 481 if (username!=null && password!=null) { 482 tr.connect(host, username, password); 483 } else { 484 tr.connect(); 485 } 486 msg.saveChanges(); 487 tr.sendMessage(msg, msg.getAllRecipients()); 488 tr.close(); 489 } 490}