View Javadoc
1   package com.randomnoun.common.email;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   import java.io.IOException;
7   import java.io.InputStream;
8   import java.util.ArrayList;
9   import java.util.Date;
10  import java.util.Iterator;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.Properties;
14  
15  import org.apache.log4j.Logger;
16  
17  import com.randomnoun.common.StreamUtil;
18  
19  import jakarta.activation.DataHandler;
20  import jakarta.activation.DataSource;
21  import jakarta.activation.FileDataSource;
22  import jakarta.mail.Message;
23  import jakarta.mail.MessagingException;
24  import jakarta.mail.Part;
25  import jakarta.mail.Session;
26  import jakarta.mail.Transport;
27  import jakarta.mail.internet.InternetAddress;
28  import jakarta.mail.internet.MimeBodyPart;
29  import jakarta.mail.internet.MimeMessage;
30  import jakarta.mail.internet.MimeMultipart;
31  
32  /**
33   * Provides a simple, one-class wrapper around the Java Mail API. Use the
34   * {@link #emailTo(String, String, String, String, String, String, String)} 
35   * method to send a mail in a single method call, or 
36   * {@link #emailToNoEx(String, String, String, String, String, String, String)} 
37   * to send an email ignoring exceptions.
38   *
39   * <p>SMTP authentication is supported.
40   * 
41   * <p>S/MIME encryption isn't supported, but would be relatively easy to add;
42   *
43   * <p>Use the (somewhat more complex) {@link #emailAttachmentTo(Map)} method to send emails
44   * with attachments sourced from the file system, byte arrays or the classpath,
45   * or to modify the email headers.
46   *
47   * @author knoxg
48   */
49  public class EmailWrapper {
50      
51      public static Logger logger = Logger.getLogger(EmailWrapper.class);
52  
53      private static boolean isBlank(String string) {
54          return (string==null || "".equals(string));
55      }
56  
57      /**
58       * A static method which provides the facility to easily send off an
59       * email using the JavaMail API. Mail-generated exceptions are sent to
60       * logger.error.
61       *
62       * @param to A comma-separated list of recipients
63       * @param from The address to place in the From: field of the email
64       * @param subject The subject text
65       * @param bodyText The message text
66       */
67      public static void emailToNoEx(String to, String from, String host, String subject, 
68        String bodyText, String username, String password) {
69          try {
70              emailTo(to, from, host, subject, bodyText, username, password);
71          } catch (MessagingException me) {
72              logger.error("A messaging exception occurred whilst sending an email", me);
73          }
74      }
75  
76      /**
77       * A simpler version of emailTo. 
78       *
79       * @param to A comma-separated list of recipients
80       * @param from The address to place in the From: field of the email
81       * @param host The SMTP host to use
82       * @param subject The subject text
83       * @param bodyText The message text
84       * @param username The SMTP user to send from (null if not authenticated)
85       * @param password The SMTP password to use (null if not authenticated)
86       *
87       */
88      public static void emailTo(String to, String from, String host, String subject, String bodyText,
89          String username, String password)
90          throws MessagingException {
91          Properties props;
92  
93          props = new Properties();
94          props.put("to", to);
95          props.put("from", from);
96          props.put("host", host);
97          props.put("subject", subject);
98          props.put("bodyText", bodyText);
99          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 &amp; 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 }