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 &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}