001/*
002 *  Copyright 2014 Anyware Services
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 *      http://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 */
016package org.ametys.core.util.mail;
017
018import java.io.File;
019import java.io.IOException;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Date;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Properties;
026import java.util.StringTokenizer;
027import java.util.concurrent.ExecutorService;
028import java.util.concurrent.Executors;
029import java.util.concurrent.ThreadFactory;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033import javax.mail.Message;
034import javax.mail.MessagingException;
035import javax.mail.Multipart;
036import javax.mail.Session;
037import javax.mail.Transport;
038import javax.mail.internet.InternetAddress;
039import javax.mail.internet.MimeBodyPart;
040import javax.mail.internet.MimeMessage;
041import javax.mail.internet.MimeMultipart;
042
043import org.apache.avalon.framework.activity.Disposable;
044import org.apache.avalon.framework.logger.AbstractLogEnabled;
045import org.apache.commons.lang3.StringUtils;
046import org.jsoup.Jsoup;
047import org.jsoup.nodes.Document;
048import org.jsoup.nodes.Element;
049import org.jsoup.select.Elements;
050import org.jsoup.select.Selector;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054import org.ametys.runtime.config.Config;
055
056/**
057 * Helper for sending mail
058 */
059public final class SendMailHelper extends AbstractLogEnabled implements Disposable
060{
061    /** Logger */
062    protected static final Logger _LOGGER = LoggerFactory.getLogger(SendMailHelper.class);
063
064    /** Attribute selectors pattern for CSS specificity processing */
065    protected static final Pattern __CSS_SPECIFICITY_ATTR_PATTERN = Pattern.compile("(\\[[^\\]]+\\])");
066    /** ID selectors pattern for CSS specificity processing */
067    protected static final Pattern __CSS_SPECIFICITY_ID_PATTERN = Pattern.compile("(#[^\\s\\+>~\\.\\[:]+)");
068    /** Class selectors pattern for CSS specificity processing */
069    protected static final Pattern __CSS_SPECIFICITY_CLASS_PATTERN = Pattern.compile("(\\.[^\\s\\+>~\\.\\[:]+)");
070    /** Pseudo-element selectors pattern for CSS specificity processing */
071    protected static final Pattern __CSS_SPECIFICITY_PSEUDO_ELEMENT_PATTERN = Pattern.compile("(::[^\\s\\+>~\\.\\[:]+|:first-line|:first-letter|:before|:after)", Pattern.CASE_INSENSITIVE);
072    /** Pseudo-class (with bracket) selectors pattern for CSS specificity processing */
073    protected static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_WITH_BRACKETS_PATTERN = Pattern.compile("(:[\\w-]+\\([^\\)]*\\))", Pattern.CASE_INSENSITIVE);
074    /** Pseudo-class selectors pattern for CSS specificity processing */
075    protected static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_PATTERN = Pattern.compile("(:[^\\s\\+>~\\.\\[:]+)");
076    /** Element selectors pattern for CSS specificity processing */
077    protected static final Pattern __CSS_SPECIFICITY_ELEMENT_PATTERN = Pattern.compile("([^\\s\\+>~\\.\\[:]+)");
078    
079    /** Specific :not pseudo-class selectors pattern for CSS specificity processing */
080    protected static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_NOT_PATTERN = Pattern.compile(":not\\(([^\\)]*)\\)");
081    /** Universal and separator characters pattern for CSS specificity processing */
082    protected static final Pattern __CSS_SPECIFICITY_UNIVERSAL_AND_SEPARATOR_PATTERN = Pattern.compile("[\\*\\s\\+>~]");
083
084    private static final ExecutorService __SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor(new MailSenderThreadFactory());
085    
086    private SendMailHelper ()
087    {
088        // Nothing
089    }
090
091    /**
092     * Sends mail without authentication or attachments.
093     * @param subject The mail subject
094     * @param htmlBody The HTML mail body. Can be null.
095     * @param textBody The text mail body. Can be null.
096     * @param recipient The recipient address
097     * @param sender The sender address
098     * @throws MessagingException If an error occurred while preparing or sending email
099     */
100    public static void sendMail(String subject, String htmlBody, String textBody, String recipient, String sender) throws MessagingException
101    {
102        sendMail(subject, htmlBody, textBody, recipient, sender, false);
103    }
104    
105    /**
106     * Sends mail without authentication or attachments.
107     * @param subject The mail subject
108     * @param htmlBody The HTML mail body. Can be null.
109     * @param textBody The text mail body. Can be null.
110     * @param recipient The recipient address
111     * @param sender The sender address
112     * @param async True to use asynchronous mail sending
113     * @throws MessagingException If an error occurred while preparing or sending email
114     */
115    public static void sendMail(String subject, String htmlBody, String textBody, String recipient, String sender, boolean async) throws MessagingException
116    {
117        String smtpHost = Config.getInstance().getValueAsString("smtp.mail.host");
118        long smtpPort = Config.getInstance().getValueAsLong("smtp.mail.port");
119        String securityProtocol = Config.getInstance().getValueAsString("smtp.mail.security.protocol");
120
121        sendMail(subject, htmlBody, textBody, recipient, sender, smtpHost, smtpPort, securityProtocol, async);
122    }
123    
124    /**
125     * Sends mail without authentication or attachments.
126     * @param subject The mail subject
127     * @param htmlBody The HTML mail body. Can be null.
128     * @param textBody The text mail body. Can be null.
129     * @param recipient The recipient address
130     * @param sender The sender address
131     * @param host The server mail host
132     * @param port The server mail port
133     * @param securityProtocol The server mail security protocol
134     * @param async True to use asynchronous mail sending
135     * @throws MessagingException If an error occurred while preparing or sending email
136     */
137    public static void sendMail(String subject, String htmlBody, String textBody, String recipient, String sender, String host, long port, String securityProtocol, boolean async) throws MessagingException
138    {
139        String user = Config.getInstance().getValueAsString("smtp.mail.user");
140        String password = Config.getInstance().getValueAsString("smtp.mail.password");
141        sendMail(subject, htmlBody, textBody, recipient, sender, host, port, securityProtocol, user, password, async);
142    }
143
144
145    /**
146     * Sends mail with authentication, without attachments.
147     * @param subject The mail subject
148     * @param htmlBody The HTML mail body. Can be null.
149     * @param textBody The text mail body. Can be null.
150     * @param recipient The recipient address
151     * @param sender The sender address
152     * @param host The server mail host
153     * @param port The server port
154     * @param securityProtocol The server mail security protocol
155     * @param user The user name
156     * @param password The user password
157     * @param async True to use asynchronous mail sending
158     * @throws MessagingException If an error occurred while preparing or sending email
159     */
160    public static void sendMail(String subject, String htmlBody, String textBody, String recipient, String sender, String host, long port, String securityProtocol, String user, String password, boolean async) throws MessagingException
161    {
162        try
163        {
164            String sp = StringUtils.defaultIfEmpty(securityProtocol, Config.getInstance().getValueAsString("smtp.mail.security.protocol"));
165            sendMail(subject, htmlBody, textBody, null, recipient, sender, null, null, false, false, host, port, sp, user, password, async);
166        }
167        catch (IOException e)
168        {
169            // Should never happen, as IOException can only be thrown where there are attachments.
170            _LOGGER.error("Cannot send mail " + subject + " to " + recipient, e);
171        }
172    }
173
174    /**
175     * Sends mail without authentication, with attachments.
176     * @param subject The mail subject
177     * @param htmlBody The HTML mail body. Can be null.
178     * @param textBody The text mail body. Can be null.
179     * @param attachments the file attachments. Can be null.
180     * @param recipient The recipient address
181     * @param sender The sender address
182     * @throws MessagingException If an error occurred while preparing or sending email
183     * @throws IOException if an error occurs while attaching a file.
184     */
185    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, String recipient, String sender) throws MessagingException, IOException
186    {
187        sendMail(subject, htmlBody, textBody, attachments, recipient, sender, null, null, false);
188    }
189    
190    /**
191     * Sends mail without authentication, with attachments.
192     * @param subject The mail subject
193     * @param htmlBody The HTML mail body. Can be null.
194     * @param textBody The text mail body. Can be null.
195     * @param attachments the file attachments. Can be null.
196     * @param recipient The recipient address
197     * @param sender The sender address
198     * @param cc Carbon copy address list. Can be null.
199     * @param bcc Blind carbon copy address list. Can be null.
200     * @param async True to use asynchronous mail sending
201     * @throws MessagingException If an error occurred while preparing or sending email
202     * @throws IOException if an error occurs while attaching a file.
203     */
204    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, String recipient, String sender, List<String> cc, List<String> bcc, boolean async) throws MessagingException, IOException
205    {
206        sendMail(subject, htmlBody, textBody, attachments, recipient, sender, cc, bcc, false, false, async);
207    }
208    
209    /**
210     * Sends mail without authentication, with attachments.
211     * @param subject The mail subject
212     * @param htmlBody The HTML mail body. Can be null.
213     * @param textBody The text mail body. Can be null.
214     * @param attachments the file attachments. Can be null.
215     * @param recipient The recipient address
216     * @param sender The sender address
217     * @param cc Carbon copy address list. Can be null.
218     * @param bcc Blind carbon copy address list. Can be null.
219     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
220     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
221     * @param async True to use asynchronous mail sending
222     * @throws MessagingException If an error occurred while preparing or sending email
223     * @throws IOException if an error occurs while attaching a file.
224     */
225    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, String recipient, String sender, List<String> cc, List<String> bcc, boolean deliveryReceipt, boolean readReceipt, boolean async) throws MessagingException, IOException
226    {
227        String smtpHost = Config.getInstance().getValueAsString("smtp.mail.host");
228        long smtpPort = Config.getInstance().getValueAsLong("smtp.mail.port");
229        String protocol = Config.getInstance().getValueAsString("smtp.mail.security.protocol");
230        
231        sendMail(subject, htmlBody, textBody, attachments, recipient, sender, cc, bcc, deliveryReceipt, readReceipt, smtpHost, smtpPort, protocol, null, null, async);
232    }
233
234    /**
235     * Sends mail without authentication, with attachments.
236     * @param subject The mail subject
237     * @param htmlBody The HTML mail body. Can be null.
238     * @param textBody The text mail body. Can be null.
239     * @param attachments the file attachments. Can be null.
240     * @param recipient The recipient address
241     * @param sender The sender address
242     * @param cc Carbon copy address list. Can be null.
243     * @param bcc Blind carbon copy address list. Can be null.
244     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
245     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
246     * @param host The server mail host
247     * @param port The server port
248     * @param securityProtocol The server mail security protocol
249     * @param async True to use asynchronous mail sending
250     * @throws MessagingException If an error occurred while preparing or sending email
251     * @throws IOException if an error occurs while attaching a file.
252     */
253    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, String recipient, String sender, List<String> cc, List<String> bcc, boolean deliveryReceipt, boolean readReceipt, String host, long port, String securityProtocol, boolean async) throws MessagingException, IOException
254    {
255        String protocol = Config.getInstance().getValueAsString("smtp.mail.security.protocol");
256        sendMail(subject, htmlBody, textBody, attachments, recipient, sender, cc, bcc, deliveryReceipt, readReceipt, host, port, protocol, null, null, async);
257    }
258    
259    /**
260     * Sends mail with authentication and attachments.
261     * @param subject The mail subject
262     * @param htmlBody The HTML mail body. Can be null.
263     * @param textBody The text mail body. Can be null.
264     * @param attachments the file attachments. Can be null.
265     * @param recipient The recipient address
266     * @param sender The sender address. Can be null when called by MailChecker.
267     * @param cc Carbon copy address list. Can be null.
268     * @param bcc Blind carbon copy address list. Can be null.
269     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
270     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
271     * @param host The server mail host. Can be null when called by MailChecker.
272     * @param securityProtocol the security protocol to use when transporting the email
273     * @param port The server port
274     * @param user The user name
275     * @param password The user password
276     * @param async True to use asynchronous mail sending
277     * @throws MessagingException If an error occurred while preparing or sending email
278     * @throws IOException if an error occurs while attaching a file.
279     */
280    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, String recipient, String sender, List<String> cc, List<String> bcc, boolean deliveryReceipt, boolean readReceipt, String host, long port, String securityProtocol, String user, String password, boolean async) throws MessagingException, IOException
281    {
282        MailSender mailSender = new MailSender(_LOGGER, subject, htmlBody, textBody, attachments, recipient, sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, user, password);
283        
284        if (!async)
285        {
286            mailSender.sendMail();
287        }
288        else
289        {
290            __SINGLE_THREAD_EXECUTOR.execute(mailSender);
291        }
292    }
293
294    /**
295     * This method inline css in &lt;style&gt; tags directly in the appropriates tags. e.g. : &lt;style&gt;h1 {color: red;}&lt;/style&gt; &lt;h1&gt;a&lt;/h1&gt; becomes &lt;h1 style="color: red"&gt;a&lt;/h1&gt;
296     * @param html The initial non null html
297     * @return The inlined html
298     */
299    public static String inlineCSS(String html)
300    {
301        List<CssRule> rules = new LinkedList<>();
302        
303        Document doc = Jsoup.parse(html); 
304        Elements els = doc.select("style");
305
306        for (Element e : els) 
307        { 
308            String styleRules = e.getAllElements().get(0).data();
309            styleRules = styleRules.replaceAll("\t|\n", "").replaceAll("<!--", "").replaceAll("-->", "");
310
311            styleRules = _removeComments(styleRules);
312
313            styleRules = styleRules.trim();
314
315            StringTokenizer st = new StringTokenizer(styleRules, "{}"); 
316            while (st.countTokens() > 1) 
317            { 
318                String selectors = st.nextToken();
319                String properties = st.nextToken();
320
321                String[] selector = selectors.split(",");
322                for (String s : selector)
323                {
324                    if (StringUtils.isNotBlank(s))
325                    {
326                        rules.add(new CssRule(s, properties, rules.size()));
327                    }
328                }
329            } 
330            e.remove(); 
331        }
332        
333        // Sort rules by specificity
334        Collections.sort(rules, Collections.reverseOrder());
335    
336        for (CssRule rule : rules)
337        {
338            try
339            {
340                Elements selectedElements = doc.select(rule.getSelector());
341                for (Element selElem : selectedElements)
342                {
343                    String oldProperties = selElem.attr("style");
344                    selElem.attr("style", oldProperties.length() > 0 ? concatenateProperties(oldProperties, rule.getProperties()) : rule.getProperties());
345                }
346            }
347            catch (Selector.SelectorParseException ex)
348            {
349                _LOGGER.error("Cannot inline CSS. Ignoring this rule and continuing.", ex);
350            }
351        }
352
353        return doc.toString();
354    }
355
356    private static String _removeComments(String styleRules)
357    {
358        int i = styleRules.indexOf("/*");
359        int j = styleRules.indexOf("*/");
360
361        if (i >= 0 && j > i)
362        {
363            return styleRules.substring(0, i) + _removeComments(styleRules.substring(j + 2));
364        }
365
366        return styleRules;
367    }
368
369    private static String concatenateProperties(String oldProp, String newProp) 
370    { 
371        String between = "";
372        if (!newProp.endsWith(";"))
373        {
374            between += ";";
375        }
376        return newProp + between + oldProp.trim(); // The existing (old) properties should take precedence. 
377    } 
378    
379    @Override
380    public void dispose()
381    {
382        __SINGLE_THREAD_EXECUTOR.shutdownNow();
383    }
384    
385    private static class CssRule implements Comparable<CssRule>
386    {
387        private String _selector;
388        private String _properties;
389        private CssSpecificity _specificity;
390        
391        /**
392         * CSSRule constructor
393         * @param selector css selector
394         * @param properties css properties for this rule
395         * @param positionIdx The rules declaration index
396         */
397        public CssRule(String selector, String properties, int positionIdx)
398        {
399            _selector = selector;
400            _properties = properties;
401            _specificity = new CssSpecificity(_selector, positionIdx);
402        }
403        
404        /**
405         * Selector getter
406         * @return the selector
407         */
408        public String getSelector()
409        {
410            return _selector;
411        }
412        
413        /**
414         * Properties getter
415         * @return the properties
416         */
417        public String getProperties()
418        {
419            return _properties;
420        }
421        
422        public int compareTo(CssRule r)
423        {
424            return _specificity.compareTo(r._specificity);
425        }
426    }
427    
428    private static class CssSpecificity implements Comparable<CssSpecificity>
429    {
430        private int[] _weights;
431        
432        public CssSpecificity(String selector, int positionIdx)
433        {
434            // Position index is used to differentiate equality cases
435            // -> latest declaration should be the one applied
436            _weights = new int[]{0, 0, 0, 0, positionIdx};
437            
438            String input = selector;
439            
440            // This part is loosely based on https://github.com/keeganstreet/specificity
441            
442            // Remove :not pseudo-class but leave its argument
443            input = __CSS_SPECIFICITY_PSEUDO_CLASS_NOT_PATTERN.matcher(input).replaceAll(" $1 ");
444            
445            // The following regular expressions assume that selectors matching the preceding regular expressions have been removed
446            input = _countReplaceAll(__CSS_SPECIFICITY_ATTR_PATTERN, input, 2);
447            input = _countReplaceAll(__CSS_SPECIFICITY_ID_PATTERN, input, 1);
448            input = _countReplaceAll(__CSS_SPECIFICITY_CLASS_PATTERN, input, 2);
449            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_ELEMENT_PATTERN, input, 3);
450            // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang()
451            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_CLASS_WITH_BRACKETS_PATTERN, input, 2);
452            // A regex for other pseudo classes, which don't have brackets
453            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_CLASS_PATTERN, input, 2);
454            
455            // Remove universal selector and separator characters
456            input = __CSS_SPECIFICITY_UNIVERSAL_AND_SEPARATOR_PATTERN.matcher(input).replaceAll(" ");
457            
458            _countReplaceAll(__CSS_SPECIFICITY_ELEMENT_PATTERN, input, 3);
459        }
460        
461        private String _countReplaceAll(Pattern pattern, String selector, int sIndex)
462        {
463            Matcher m = pattern.matcher(selector);
464            StringBuffer sb = new StringBuffer();
465            
466            while (m.find())
467            {
468                // Increment desired weight counter
469                _weights[sIndex]++;
470                
471                // Replace matched selector part with whitespace
472                m.appendReplacement(sb, " ");
473            }
474            
475            m.appendTail(sb);
476            
477            return sb.toString();
478        }
479        
480        public int compareTo(CssSpecificity o)
481        {
482            for (int i = 0; i < _weights.length; i++)
483            {
484                if (_weights[i] != o._weights[i])
485                {
486                    return _weights[i] - o._weights[i];
487                }
488            }
489            
490            return 0;
491        }
492    }
493    
494    private static class MailSender implements Runnable
495    {
496        private String _subject;
497        private String _htmlBody;
498        private String _textBody;
499        private Collection<File> _attachments;
500        private String _recipient;
501        private String _sender;
502        private List<String> _cc;
503        private List<String> _bcc;
504        private boolean _deliveryReceipt;
505        private boolean _readReceipt;
506        private String _host;
507        private long _port;
508        private String _securityProtocol;
509        private String _user;
510        private String _password;
511        private Logger _logger;
512
513        /**
514         * Initialize the mail sender with email parameters
515         * @param logger The logger
516         * @param subject The mail subject
517         * @param htmlBody The HTML mail body. Can be null.
518         * @param textBody The text mail body. Can be null.
519         * @param attachments the file attachments. Can be null.
520         * @param recipient The recipient address
521         * @param sender The sender address. Can be null when called by MailChecker.
522         * @param cc Carbon copy address list. Can be null.
523         * @param bcc Blind carbon copy address list. Can be null.
524         * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
525         * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
526         * @param host The server mail host. Can be null when called by MailChecker.
527         * @param securityProtocol the security protocol to use when transporting the email
528         * @param port The server port
529         * @param user The user name
530         * @param password The user password
531         */
532        public MailSender(Logger logger, String subject, String htmlBody, String textBody, Collection<File> attachments, String recipient, String sender, List<String> cc, List<String> bcc, boolean deliveryReceipt, boolean readReceipt, String host, long port, String securityProtocol, String user, String password)
533        {
534            _logger = logger;
535            _subject = subject;
536            _htmlBody = htmlBody;
537            _textBody = textBody;
538            _attachments = attachments;
539            _recipient = recipient;
540            _sender = sender;
541            _cc = cc;
542            _bcc = bcc;
543            _deliveryReceipt = deliveryReceipt;
544            _readReceipt = readReceipt;
545            _host = host;
546            _port = port;
547            _securityProtocol = securityProtocol;
548            _user = user;
549            _password = password;
550        }
551        
552        public void run()
553        {
554            try
555            {
556                sendMail();
557            }
558            catch (Exception e)
559            {
560                _logger.error("Unable to send mail: " + _subject + "", e);
561            }
562        }
563        
564        public void sendMail() throws MessagingException, IOException
565        {
566            Properties props = new Properties();
567
568            // Setup mail server
569            props.put("mail.smtp.host", _host);
570            props.put("mail.smtp.port", _port);
571            
572            // Security protocol
573            if (_securityProtocol.equals("starttls"))
574            {
575                props.put("mail.smtp.starttls.enable", "true"); 
576            }
577            else if (_securityProtocol.equals("tlsssl"))
578            {
579                props.put("mail.smtp.ssl.enable", "true");
580            }
581            
582            Session session = Session.getInstance(props, null);
583            
584            // Define message
585            MimeMessage message = new MimeMessage(session);
586            
587            if (_sender != null)
588            {
589                message.setFrom(new InternetAddress(_sender));
590            }
591            
592            message.setSentDate(new Date());
593            message.setSubject(_subject);
594            
595            // Root multipart
596            Multipart multipart = new MimeMultipart("mixed");
597
598            // Message body part.
599            Multipart messageMultipart = new MimeMultipart("alternative");
600            MimeBodyPart messagePart = new MimeBodyPart();
601            messagePart.setContent(messageMultipart);
602            multipart.addBodyPart(messagePart);
603
604            if (_textBody != null)
605            {
606                MimeBodyPart textBodyPart = new MimeBodyPart();
607                textBodyPart.setContent(_textBody, "text/plain;charset=utf-8");
608                textBodyPart.addHeader("Content-Type", "text/plain;charset=utf-8");
609                messageMultipart.addBodyPart(textBodyPart);
610            }
611
612            if (_htmlBody != null)
613            {
614                MimeBodyPart htmlBodyPart = new MimeBodyPart();
615                htmlBodyPart.setContent(inlineCSS(_htmlBody), "text/html;charset=utf-8");
616                htmlBodyPart.addHeader("Content-Type", "text/html;charset=utf-8");
617                messageMultipart.addBodyPart(htmlBodyPart);
618            }
619
620            if (_attachments != null)
621            {
622                for (File attachment : _attachments)
623                {
624                    MimeBodyPart fileBodyPart = new MimeBodyPart();
625                    fileBodyPart.attachFile(attachment);
626                    multipart.addBodyPart(fileBodyPart);
627                }
628            }
629            message.setContent(multipart);
630
631            // Recipients
632            if (_recipient != null)
633            {
634                message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(_recipient, false));
635            }
636            
637            // Carbon copies
638            if (_cc != null)
639            {
640                message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(StringUtils.join(_cc, ','), false));
641            }
642
643            // Blind carbon copies
644            if (_bcc != null)
645            {
646                message.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(StringUtils.join(_bcc, ','), false));
647            }
648            
649            // Delivery receipt : Return-Receipt-To
650            if (_deliveryReceipt)
651            {
652                message.setHeader("Return-Receipt-To", _sender);
653            }
654            
655            // Read receipt : Disposition-Notification-To
656            if (_readReceipt)
657            {
658                message.setHeader("Disposition-Notification-To", _sender);
659            }
660            
661            message.saveChanges();
662            
663            Transport tr = session.getTransport("smtp");
664            
665            try
666            {
667                tr.connect(_host, (int) _port, StringUtils.trimToNull(_user), StringUtils.trimToNull(_password));
668                
669                if (_recipient != null && _sender != null)
670                {
671                    tr.sendMessage(message, message.getAllRecipients());
672                }
673            }
674            finally
675            {
676                tr.close();
677            }
678        }
679    }
680    
681    private static class MailSenderThreadFactory implements ThreadFactory
682    {
683        private ThreadFactory _defaultThreadFactory;
684
685        public MailSenderThreadFactory()
686        {
687            _defaultThreadFactory = Executors.defaultThreadFactory();
688        }
689        
690        public Thread newThread(Runnable r)
691        {
692            Thread thread = _defaultThreadFactory.newThread(r);
693            thread.setName("mail-sender-thread");
694            thread.setDaemon(true);
695            
696            return thread;
697        }
698    }
699}