001/*
002 *  Copyright 2019 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    /** Regexp to validate an email */
062    public static final String EMAIL_VALIDATION_REGEXP = "^.+@.+$";
063    // No you cannot do better
064    /** Regexp to validate an email */
065    public static final Pattern EMAIL_VALIDATION = Pattern.compile(EMAIL_VALIDATION_REGEXP);
066    // No you cannot do better
067    
068    /** Logger */
069    protected static final Logger _LOGGER = LoggerFactory.getLogger(SendMailHelper.class);
070
071    /** Attribute selectors pattern for CSS specificity processing */
072    protected static final Pattern __CSS_SPECIFICITY_ATTR_PATTERN = Pattern.compile("(\\[[^\\]]+\\])");
073    /** ID selectors pattern for CSS specificity processing */
074    protected static final Pattern __CSS_SPECIFICITY_ID_PATTERN = Pattern.compile("(#[^\\s\\+>~\\.\\[:]+)");
075    /** Class selectors pattern for CSS specificity processing */
076    protected static final Pattern __CSS_SPECIFICITY_CLASS_PATTERN = Pattern.compile("(\\.[^\\s\\+>~\\.\\[:]+)");
077    /** Pseudo-element selectors pattern for CSS specificity processing */
078    protected static final Pattern __CSS_SPECIFICITY_PSEUDO_ELEMENT_PATTERN = Pattern.compile("(::[^\\s\\+>~\\.\\[:]+|:first-line|:first-letter|:before|:after)", Pattern.CASE_INSENSITIVE);
079    /** Pseudo-class (with bracket) selectors pattern for CSS specificity processing */
080    protected static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_WITH_BRACKETS_PATTERN = Pattern.compile("(:[\\w-]+\\([^\\)]*\\))", Pattern.CASE_INSENSITIVE);
081    /** Pseudo-class selectors pattern for CSS specificity processing */
082    protected static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_PATTERN = Pattern.compile("(:[^\\s\\+>~\\.\\[:]+)");
083    /** Element selectors pattern for CSS specificity processing */
084    protected static final Pattern __CSS_SPECIFICITY_ELEMENT_PATTERN = Pattern.compile("([^\\s\\+>~\\.\\[:]+)");
085    
086    /** Specific :not pseudo-class selectors pattern for CSS specificity processing */
087    protected static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_NOT_PATTERN = Pattern.compile(":not\\(([^\\)]*)\\)");
088    /** Universal and separator characters pattern for CSS specificity processing */
089    protected static final Pattern __CSS_SPECIFICITY_UNIVERSAL_AND_SEPARATOR_PATTERN = Pattern.compile("[\\*\\s\\+>~]");
090
091    private static final ExecutorService __SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor(new MailSenderThreadFactory());
092    
093    private SendMailHelper ()
094    {
095        // Nothing
096    }
097
098    /**
099     * Sends mail without authentication or attachments.
100     * @param subject The mail subject
101     * @param htmlBody The HTML mail body. Can be null.
102     * @param textBody The text mail body. Can be null.
103     * @param recipient The recipient address
104     * @param sender The sender address
105     * @throws MessagingException If an error occurred while preparing or sending email
106     */
107    public static void sendMail(String subject, String htmlBody, String textBody, String recipient, String sender) throws MessagingException
108    {
109        sendMail(subject, htmlBody, textBody, Collections.singletonList(recipient), sender);
110    }
111    
112    /**
113     * Sends mail without authentication or attachments.
114     * @param subject The mail subject
115     * @param htmlBody The HTML mail body. Can be null.
116     * @param textBody The text mail body. Can be null.
117     * @param recipients The recipients addresses
118     * @param sender The sender address
119     * @throws MessagingException If an error occurred while preparing or sending email
120     */
121    public static void sendMail(String subject, String htmlBody, String textBody, List<String> recipients, String sender) throws MessagingException
122    {
123        sendMail(subject, htmlBody, textBody, recipients, sender, false);
124    }
125    
126    /**
127     * Sends mail without authentication or attachments.
128     * @param subject The mail subject
129     * @param htmlBody The HTML mail body. Can be null.
130     * @param textBody The text mail body. Can be null.
131     * @param recipient The recipient address
132     * @param sender The sender address
133     * @param async True to use asynchronous mail sending
134     * @throws MessagingException If an error occurred while preparing or sending email
135     */
136    public static void sendMail(String subject, String htmlBody, String textBody, String recipient, String sender, boolean async) throws MessagingException
137    {
138        sendMail(subject, htmlBody, textBody, Collections.singletonList(recipient), sender, async);
139    }
140    
141    /**
142     * Sends mail without authentication or attachments.
143     * @param subject The mail subject
144     * @param htmlBody The HTML mail body. Can be null.
145     * @param textBody The text mail body. Can be null.
146     * @param recipients The recipients addresses
147     * @param sender The sender address
148     * @param async True to use asynchronous mail sending
149     * @throws MessagingException If an error occurred while preparing or sending email
150     */
151    public static void sendMail(String subject, String htmlBody, String textBody, List<String> recipients, String sender, boolean async) throws MessagingException
152    {
153        Config config = Config.getInstance();
154        String smtpHost = config.getValue("smtp.mail.host");
155        long smtpPort = config.getValue("smtp.mail.port");
156        String securityProtocol = config.getValue("smtp.mail.security.protocol");
157
158        sendMail(subject, htmlBody, textBody, recipients, sender, smtpHost, smtpPort, securityProtocol, async);
159    }
160    
161    /**
162     * Sends mail without authentication or attachments.
163     * @param subject The mail subject
164     * @param htmlBody The HTML mail body. Can be null.
165     * @param textBody The text mail body. Can be null.
166     * @param recipient The recipient address
167     * @param sender The sender address
168     * @param host The server mail host
169     * @param port The server mail port
170     * @param securityProtocol The server mail security protocol
171     * @param async True to use asynchronous mail sending
172     * @throws MessagingException If an error occurred while preparing or sending email
173     */
174    public static void sendMail(String subject, String htmlBody, String textBody, String recipient, String sender, String host, long port, String securityProtocol, boolean async) throws MessagingException
175    {
176        sendMail(subject, htmlBody, textBody, Collections.singletonList(recipient), sender, host, port, securityProtocol, async);
177    }
178    
179    /**
180     * Sends mail without authentication or attachments.
181     * @param subject The mail subject
182     * @param htmlBody The HTML mail body. Can be null.
183     * @param textBody The text mail body. Can be null.
184     * @param recipients The recipients addresses
185     * @param sender The sender address
186     * @param host The server mail host
187     * @param port The server mail port
188     * @param securityProtocol The server mail security protocol
189     * @param async True to use asynchronous mail sending
190     * @throws MessagingException If an error occurred while preparing or sending email
191     */
192    public static void sendMail(String subject, String htmlBody, String textBody, List<String> recipients, String sender, String host, long port, String securityProtocol, boolean async) throws MessagingException
193    {
194        Config config = Config.getInstance();
195        String user = config.getValue("smtp.mail.user");
196        String password = config.getValue("smtp.mail.password");
197        sendMail(subject, htmlBody, textBody, recipients, sender, host, port, securityProtocol, user, password, async);
198    }
199
200    /**
201     * Sends mail with authentication, without attachments.
202     * @param subject The mail subject
203     * @param htmlBody The HTML mail body. Can be null.
204     * @param textBody The text mail body. Can be null.
205     * @param recipient The recipient address
206     * @param sender The sender address
207     * @param host The server mail host
208     * @param port The server port
209     * @param securityProtocol The server mail security protocol
210     * @param user The user name
211     * @param password The user password
212     * @param async True to use asynchronous mail sending
213     * @throws MessagingException If an error occurred while preparing or sending email
214     */
215    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
216    {
217        sendMail(subject, htmlBody, textBody, Collections.singletonList(recipient), sender, host, port, securityProtocol, user, password, async);
218    }
219    
220    /**
221     * Sends mail with authentication, without attachments.
222     * @param subject The mail subject
223     * @param htmlBody The HTML mail body. Can be null.
224     * @param textBody The text mail body. Can be null.
225     * @param recipients The recipients addresses
226     * @param sender The sender address
227     * @param host The server mail host
228     * @param port The server port
229     * @param securityProtocol The server mail security protocol
230     * @param user The user name
231     * @param password The user password
232     * @param async True to use asynchronous mail sending
233     * @throws MessagingException If an error occurred while preparing or sending email
234     */
235    public static void sendMail(String subject, String htmlBody, String textBody, List<String> recipients, String sender, String host, long port, String securityProtocol, String user, String password, boolean async) throws MessagingException
236    {
237        try
238        {
239            String sp = StringUtils.defaultIfEmpty(securityProtocol, Config.getInstance().getValue("smtp.mail.security.protocol"));
240            sendMail(subject, htmlBody, textBody, null, recipients, sender, null, null, false, false, host, port, sp, user, password, async);
241        }
242        catch (IOException e)
243        {
244            // Should never happen, as IOException can only be thrown where there are attachments.
245            _LOGGER.error("Cannot send mail " + subject + " to " + recipients, e);
246        }
247    }
248
249    /**
250     * Sends mail without authentication, with attachments.
251     * @param subject The mail subject
252     * @param htmlBody The HTML mail body. Can be null.
253     * @param textBody The text mail body. Can be null.
254     * @param attachments the file attachments. Can be null.
255     * @param recipient The recipient address
256     * @param sender The sender address
257     * @throws MessagingException If an error occurred while preparing or sending email
258     * @throws IOException if an error occurs while attaching a file.
259     */
260    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, String recipient, String sender) throws MessagingException, IOException
261    {
262        sendMail(subject, htmlBody, textBody, attachments, Collections.singletonList(recipient), sender);
263    }
264    
265    /**
266     * Sends mail without authentication, with attachments.
267     * @param subject The mail subject
268     * @param htmlBody The HTML mail body. Can be null.
269     * @param textBody The text mail body. Can be null.
270     * @param attachments the file attachments. Can be null.
271     * @param recipients The recipients addresses
272     * @param sender The sender address
273     * @throws MessagingException If an error occurred while preparing or sending email
274     * @throws IOException if an error occurs while attaching a file.
275     */
276    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, List<String> recipients, String sender) throws MessagingException, IOException
277    {
278        sendMail(subject, htmlBody, textBody, attachments, recipients, sender, null, null, false);
279    }
280    
281    /**
282     * Sends mail without authentication, with attachments.
283     * @param subject The mail subject
284     * @param htmlBody The HTML mail body. Can be null.
285     * @param textBody The text mail body. Can be null.
286     * @param attachments the file attachments. Can be null.
287     * @param recipient The recipient address
288     * @param sender The sender address
289     * @param cc Carbon copy address list. Can be null.
290     * @param bcc Blind carbon copy address list. Can be null.
291     * @param async True to use asynchronous mail sending
292     * @throws MessagingException If an error occurred while preparing or sending email
293     * @throws IOException if an error occurs while attaching a file.
294     */
295    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
296    {
297        sendMail(subject, htmlBody, textBody, attachments, Collections.singletonList(recipient), sender, cc, bcc, async);
298    }
299    
300    /**
301     * Sends mail without authentication, with attachments.
302     * @param subject The mail subject
303     * @param htmlBody The HTML mail body. Can be null.
304     * @param textBody The text mail body. Can be null.
305     * @param attachments the file attachments. Can be null.
306     * @param recipients The recipients addresses
307     * @param sender The sender address
308     * @param cc Carbon copy address list. Can be null.
309     * @param bcc Blind carbon copy address list. Can be null.
310     * @param async True to use asynchronous mail sending
311     * @throws MessagingException If an error occurred while preparing or sending email
312     * @throws IOException if an error occurs while attaching a file.
313     */
314    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, List<String> recipients, String sender, List<String> cc, List<String> bcc, boolean async) throws MessagingException, IOException
315    {
316        sendMail(subject, htmlBody, textBody, attachments, recipients, sender, cc, bcc, false, false, async);
317    }
318    
319    /**
320     * Sends mail without authentication, with attachments.
321     * @param subject The mail subject
322     * @param htmlBody The HTML mail body. Can be null.
323     * @param textBody The text mail body. Can be null.
324     * @param attachments the file attachments. Can be null.
325     * @param recipient The recipient address
326     * @param sender The sender address
327     * @param cc Carbon copy address list. Can be null.
328     * @param bcc Blind carbon copy address list. Can be null.
329     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
330     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
331     * @param async True to use asynchronous mail sending
332     * @throws MessagingException If an error occurred while preparing or sending email
333     * @throws IOException if an error occurs while attaching a file.
334     */
335    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
336    {
337        sendMail(subject, htmlBody, textBody, attachments, Collections.singletonList(recipient), sender, cc, bcc, deliveryReceipt, readReceipt, async);
338    }
339
340    /**
341     * Sends mail without authentication, with attachments.
342     * @param subject The mail subject
343     * @param htmlBody The HTML mail body. Can be null.
344     * @param textBody The text mail body. Can be null.
345     * @param attachments the file attachments. Can be null.
346     * @param recipients The recipients addresses
347     * @param sender The sender address
348     * @param cc Carbon copy address list. Can be null.
349     * @param bcc Blind carbon copy address list. Can be null.
350     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
351     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
352     * @param async True to use asynchronous mail sending
353     * @throws MessagingException If an error occurred while preparing or sending email
354     * @throws IOException if an error occurs while attaching a file.
355     */
356    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, List<String> recipients, String sender, List<String> cc, List<String> bcc, boolean deliveryReceipt, boolean readReceipt, boolean async) throws MessagingException, IOException
357    {
358        Config config = Config.getInstance();
359        String smtpHost = config.getValue("smtp.mail.host");
360        long smtpPort = config.getValue("smtp.mail.port");
361        String protocol = config.getValue("smtp.mail.security.protocol");
362        String user = config.getValue("smtp.mail.user");
363        String password = config.getValue("smtp.mail.password");
364        
365        sendMail(subject, htmlBody, textBody, attachments, recipients, sender, cc, bcc, deliveryReceipt, readReceipt, smtpHost, smtpPort, protocol, user, password, async);
366    }
367    
368    /**
369     * Sends mail without authentication, with attachments.
370     * @param subject The mail subject
371     * @param htmlBody The HTML mail body. Can be null.
372     * @param textBody The text mail body. Can be null.
373     * @param attachments the file attachments. Can be null.
374     * @param recipient The recipient address
375     * @param sender The sender address
376     * @param cc Carbon copy address list. Can be null.
377     * @param bcc Blind carbon copy address list. Can be null.
378     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
379     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
380     * @param host The server mail host
381     * @param port The server port
382     * @param securityProtocol The server mail security protocol
383     * @param async True to use asynchronous mail sending
384     * @throws MessagingException If an error occurred while preparing or sending email
385     * @throws IOException if an error occurs while attaching a file.
386     */
387    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
388    {
389        sendMail(subject, htmlBody, textBody, attachments, Collections.singletonList(recipient), sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, async);
390    }
391    
392    /**
393     * Sends mail without authentication, with attachments.
394     * @param subject The mail subject
395     * @param htmlBody The HTML mail body. Can be null.
396     * @param textBody The text mail body. Can be null.
397     * @param attachments the file attachments. Can be null.
398     * @param recipients The recipients addresses
399     * @param sender The sender address
400     * @param cc Carbon copy address list. Can be null.
401     * @param bcc Blind carbon copy address list. Can be null.
402     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
403     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
404     * @param host The server mail host
405     * @param port The server port
406     * @param securityProtocol The server mail security protocol
407     * @param async True to use asynchronous mail sending
408     * @throws MessagingException If an error occurred while preparing or sending email
409     * @throws IOException if an error occurs while attaching a file.
410     */
411    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, List<String> recipients, String sender, List<String> cc, List<String> bcc, boolean deliveryReceipt, boolean readReceipt, String host, long port, String securityProtocol, boolean async) throws MessagingException, IOException
412    {
413        Config config = Config.getInstance();
414        String user = config.getValue("smtp.mail.user");
415        String password = config.getValue("smtp.mail.password");
416
417        sendMail(subject, htmlBody, textBody, attachments, recipients, sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, user, password, async);
418    }
419    
420    /**
421     * Sends mail with authentication and attachments.
422     * @param subject The mail subject
423     * @param htmlBody The HTML mail body. Can be null.
424     * @param textBody The text mail body. Can be null.
425     * @param attachments the file attachments. Can be null.
426     * @param recipient The recipient address
427     * @param sender The sender address. Can be null when called by MailChecker.
428     * @param cc Carbon copy address list. Can be null.
429     * @param bcc Blind carbon copy address list. Can be null.
430     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
431     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
432     * @param host The server mail host. Can be null when called by MailChecker.
433     * @param securityProtocol the security protocol to use when transporting the email
434     * @param port The server port
435     * @param user The user name
436     * @param password The user password
437     * @param async True to use asynchronous mail sending
438     * @throws MessagingException If an error occurred while preparing or sending email
439     * @throws IOException if an error occurs while attaching a file.
440     */
441    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
442    {
443        sendMail(subject, htmlBody, textBody, attachments, Collections.singletonList(recipient), sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, user, password, async);
444    }
445
446    /**
447     * Sends mail with authentication and attachments.
448     * @param subject The mail subject
449     * @param htmlBody The HTML mail body. Can be null.
450     * @param textBody The text mail body. Can be null.
451     * @param attachments the file attachments. Can be null.
452     * @param recipients The recipients addresses
453     * @param sender The sender address. Can be null when called by MailChecker.
454     * @param cc Carbon copy address list. Can be null.
455     * @param bcc Blind carbon copy address list. Can be null.
456     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
457     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
458     * @param host The server mail host. Can be null when called by MailChecker.
459     * @param securityProtocol the security protocol to use when transporting the email
460     * @param port The server port
461     * @param user The user name
462     * @param password The user password
463     * @param async True to use asynchronous mail sending
464     * @throws MessagingException If an error occurred while preparing or sending email
465     * @throws IOException if an error occurs while attaching a file.
466     */
467    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, List<String> recipients, 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
468    {
469        sendMail(subject, htmlBody, textBody, attachments, recipients, sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, user, password, async, false);
470    }
471    
472    /**
473     * Sends mail with authentication and attachments.
474     * @param subject The mail subject
475     * @param htmlBody The HTML mail body. Can be null.
476     * @param textBody The text mail body. Can be null.
477     * @param attachments the file attachments. Can be null.
478     * @param recipients The recipients addresses
479     * @param sender The sender address. Can be null when called by MailChecker.
480     * @param cc Carbon copy address list. Can be null.
481     * @param bcc Blind carbon copy address list. Can be null.
482     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
483     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
484     * @param host The server mail host. Can be null when called by MailChecker.
485     * @param securityProtocol the security protocol to use when transporting the email
486     * @param port The server port
487     * @param user The user name
488     * @param password The user password
489     * @param async True to use asynchronous mail sending
490     * @param singleEmail true if just one email is sent with all recipients. Otherwise, an email will be sent for each recipients.
491     * @throws MessagingException If an error occurred while preparing or sending email
492     * @throws IOException if an error occurs while attaching a file.
493     */
494    public static void sendMail(String subject, String htmlBody, String textBody, Collection<File> attachments, List<String> recipients, String sender, List<String> cc, List<String> bcc, boolean deliveryReceipt, boolean readReceipt, String host, long port, String securityProtocol, String user, String password, boolean async, boolean singleEmail) throws MessagingException, IOException
495    {
496        MailSender mailSender = new MailSender(_LOGGER, subject, htmlBody, textBody, attachments, recipients, sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, user, password, singleEmail);
497        
498        if (!async)
499        {
500            mailSender.sendMail();
501        }
502        else
503        {
504            __SINGLE_THREAD_EXECUTOR.execute(mailSender);
505        }
506    }
507    
508    /**
509     * 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;
510     * @param html The initial non null html
511     * @return The inlined html
512     */
513    public static String inlineCSS(String html)
514    {
515        List<CssRule> rules = new LinkedList<>();
516        
517        Document doc = Jsoup.parse(html); 
518        Elements els = doc.select("style");
519
520        for (Element e : els) 
521        { 
522            String styleRules = e.getAllElements().get(0).data();
523            styleRules = styleRules.replaceAll("\t|\r|\n", "").replaceAll("<!--", "").replaceAll("-->", "");
524
525            styleRules = _removeComments(styleRules);
526
527            styleRules = styleRules.trim();
528
529            StringTokenizer st = new StringTokenizer(styleRules, "{}"); 
530            while (st.countTokens() > 1) 
531            { 
532                String selectors = st.nextToken();
533                String properties = st.nextToken();
534
535                String[] selector = selectors.split(",");
536                for (String s : selector)
537                {
538                    if (StringUtils.isNotBlank(s))
539                    {
540                        rules.add(new CssRule(s.trim(), properties, rules.size()));
541                    }
542                }
543            } 
544            e.remove(); 
545        }
546        
547        // Sort rules by specificity
548        Collections.sort(rules, Collections.reverseOrder());
549    
550        for (CssRule rule : rules)
551        {
552            try
553            {
554                Elements selectedElements = doc.select(rule.getSelector());
555                for (Element selElem : selectedElements)
556                {
557                    String oldProperties = selElem.attr("style");
558                    selElem.attr("style", oldProperties.length() > 0 ? concatenateProperties(oldProperties, rule.getProperties()) : rule.getProperties());
559                }
560            }
561            catch (Selector.SelectorParseException ex)
562            {
563                _LOGGER.error("Cannot inline CSS with rule \"" + rule.getSelector() + "\".Ignoring this rule and continuing.", ex);
564            }
565        }
566        
567        return doc.toString();
568    }
569
570    private static String _removeComments(String styleRules)
571    {
572        int i = styleRules.indexOf("/*");
573        int j = styleRules.indexOf("*/");
574
575        if (i >= 0 && j > i)
576        {
577            return styleRules.substring(0, i) + _removeComments(styleRules.substring(j + 2));
578        }
579
580        return styleRules;
581    }
582
583    private static String concatenateProperties(String oldProp, String newProp) 
584    { 
585        String between = "";
586        if (!newProp.endsWith(";"))
587        {
588            between += ";";
589        }
590        return newProp + between + oldProp.trim(); // The existing (old) properties should take precedence. 
591    } 
592    
593    @Override
594    public void dispose()
595    {
596        __SINGLE_THREAD_EXECUTOR.shutdownNow();
597    }
598    
599    private static class CssRule implements Comparable<CssRule>
600    {
601        private String _selector;
602        private String _properties;
603        private CssSpecificity _specificity;
604        
605        /**
606         * CSSRule constructor
607         * @param selector css selector
608         * @param properties css properties for this rule
609         * @param positionIdx The rules declaration index
610         */
611        public CssRule(String selector, String properties, int positionIdx)
612        {
613            _selector = selector;
614            _properties = properties;
615            _specificity = new CssSpecificity(_selector, positionIdx);
616        }
617        
618        /**
619         * Selector getter
620         * @return the selector
621         */
622        public String getSelector()
623        {
624            return _selector;
625        }
626        
627        /**
628         * Properties getter
629         * @return the properties
630         */
631        public String getProperties()
632        {
633            return _properties;
634        }
635        
636        public int compareTo(CssRule r)
637        {
638            return _specificity.compareTo(r._specificity);
639        }
640    }
641    
642    private static class CssSpecificity implements Comparable<CssSpecificity>
643    {
644        private int[] _weights;
645        
646        public CssSpecificity(String selector, int positionIdx)
647        {
648            // Position index is used to differentiate equality cases
649            // -> latest declaration should be the one applied
650            _weights = new int[]{0, 0, 0, 0, positionIdx};
651            
652            String input = selector;
653            
654            // This part is loosely based on https://github.com/keeganstreet/specificity
655            
656            // Remove :not pseudo-class but leave its argument
657            input = __CSS_SPECIFICITY_PSEUDO_CLASS_NOT_PATTERN.matcher(input).replaceAll(" $1 ");
658            
659            // The following regular expressions assume that selectors matching the preceding regular expressions have been removed
660            input = _countReplaceAll(__CSS_SPECIFICITY_ATTR_PATTERN, input, 2);
661            input = _countReplaceAll(__CSS_SPECIFICITY_ID_PATTERN, input, 1);
662            input = _countReplaceAll(__CSS_SPECIFICITY_CLASS_PATTERN, input, 2);
663            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_ELEMENT_PATTERN, input, 3);
664            // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang()
665            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_CLASS_WITH_BRACKETS_PATTERN, input, 2);
666            // A regex for other pseudo classes, which don't have brackets
667            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_CLASS_PATTERN, input, 2);
668            
669            // Remove universal selector and separator characters
670            input = __CSS_SPECIFICITY_UNIVERSAL_AND_SEPARATOR_PATTERN.matcher(input).replaceAll(" ");
671            
672            _countReplaceAll(__CSS_SPECIFICITY_ELEMENT_PATTERN, input, 3);
673        }
674        
675        private String _countReplaceAll(Pattern pattern, String selector, int sIndex)
676        {
677            Matcher m = pattern.matcher(selector);
678            StringBuffer sb = new StringBuffer();
679            
680            while (m.find())
681            {
682                // Increment desired weight counter
683                _weights[sIndex]++;
684                
685                // Replace matched selector part with whitespace
686                m.appendReplacement(sb, " ");
687            }
688            
689            m.appendTail(sb);
690            
691            return sb.toString();
692        }
693        
694        public int compareTo(CssSpecificity o)
695        {
696            for (int i = 0; i < _weights.length; i++)
697            {
698                if (_weights[i] != o._weights[i])
699                {
700                    return _weights[i] - o._weights[i];
701                }
702            }
703            
704            return 0;
705        }
706    }
707    
708    private static class MailSender implements Runnable
709    {
710        private String _subject;
711        private String _htmlBody;
712        private String _textBody;
713        private Collection<File> _attachments;
714        private List<String> _recipients;
715        private String _sender;
716        private List<String> _cc;
717        private List<String> _bcc;
718        private boolean _deliveryReceipt;
719        private boolean _readReceipt;
720        private String _host;
721        private long _port;
722        private String _securityProtocol;
723        private String _user;
724        private String _password;
725        private boolean _singleEmail;
726        private Logger _logger;
727
728        /**
729         * Initialize the mail sender with email parameters
730         * @param logger The logger
731         * @param subject The mail subject
732         * @param htmlBody The HTML mail body. Can be null.
733         * @param textBody The text mail body. Can be null.
734         * @param attachments the file attachments. Can be null.
735         * @param recipients The recipients addresses
736         * @param sender The sender address. Can be null when called by MailChecker.
737         * @param cc Carbon copy address list. Can be null.
738         * @param bcc Blind carbon copy address list. Can be null.
739         * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
740         * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
741         * @param host The server mail host. Can be null when called by MailChecker.
742         * @param securityProtocol the security protocol to use when transporting the email
743         * @param port The server port
744         * @param user The user name
745         * @param password The user password
746         * @param singleEmail true if just one email is sent with all recipients. Otherwise, an email will be sent for each recipients.
747         */
748        public MailSender(Logger logger, String subject, String htmlBody, String textBody, Collection<File> attachments, List<String> recipients, String sender, List<String> cc, List<String> bcc, boolean deliveryReceipt, boolean readReceipt, String host, long port, String securityProtocol, String user, String password, boolean singleEmail)
749        {
750            _logger = logger;
751            _subject = subject;
752            _htmlBody = htmlBody;
753            _textBody = textBody;
754            _attachments = attachments;
755            _recipients = recipients;
756            _sender = sender;
757            _cc = cc;
758            _bcc = bcc;
759            _deliveryReceipt = deliveryReceipt;
760            _readReceipt = readReceipt;
761            _host = host;
762            _port = port;
763            _securityProtocol = securityProtocol;
764            _user = user;
765            _password = password;
766            _singleEmail = singleEmail;
767        }
768        
769        public void run()
770        {
771            try
772            {
773                sendMail();
774            }
775            catch (Exception e)
776            {
777                _logger.error("Unable to send mail with subject: {}", _subject, e);
778            }
779        }
780        
781        public void sendMail() throws MessagingException, IOException
782        {
783            Properties props = new Properties();
784
785            // Setup mail server
786            props.put("mail.smtp.host", _host);
787            props.put("mail.smtp.port", _port);
788            
789            // Security protocol
790            if (_securityProtocol.equals("starttls"))
791            {
792                props.put("mail.smtp.starttls.enable", "true"); 
793            }
794            else if (_securityProtocol.equals("tlsssl"))
795            {
796                props.put("mail.smtp.ssl.enable", "true");
797            }
798            
799            Session session = Session.getInstance(props, null);
800            
801            // Define message
802            MimeMessage message = new MimeMessage(session);
803            
804            if (_sender != null)
805            {
806                message.setFrom(new InternetAddress(_sender));
807            }
808            
809            message.setSentDate(new Date());
810            message.setSubject(_subject, "UTF-8");
811            
812            // Root multipart
813            Multipart multipart = new MimeMultipart("mixed");
814
815            // Message body part.
816            Multipart messageMultipart = new MimeMultipart("alternative");
817            MimeBodyPart messagePart = new MimeBodyPart();
818            messagePart.setContent(messageMultipart);
819            multipart.addBodyPart(messagePart);
820
821            if (_textBody != null)
822            {
823                MimeBodyPart textBodyPart = new MimeBodyPart();
824                textBodyPart.setContent(_textBody, "text/plain;charset=utf-8");
825                textBodyPart.addHeader("Content-Type", "text/plain;charset=utf-8");
826                messageMultipart.addBodyPart(textBodyPart);
827            }
828
829            if (_htmlBody != null)
830            {
831                MimeBodyPart htmlBodyPart = new MimeBodyPart();
832                htmlBodyPart.setContent(inlineCSS(_htmlBody), "text/html;charset=utf-8");
833                htmlBodyPart.addHeader("Content-Type", "text/html;charset=utf-8");
834                messageMultipart.addBodyPart(htmlBodyPart);
835            }
836
837            if (_attachments != null)
838            {
839                for (File attachment : _attachments)
840                {
841                    MimeBodyPart fileBodyPart = new MimeBodyPart();
842                    fileBodyPart.attachFile(attachment);
843                    multipart.addBodyPart(fileBodyPart);
844                }
845            }
846            message.setContent(multipart);
847
848            // Carbon copies
849            if (_cc != null)
850            {
851                message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(StringUtils.join(_cc, ','), false));
852            }
853
854            // Blind carbon copies
855            if (_bcc != null)
856            {
857                message.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(StringUtils.join(_bcc, ','), false));
858            }
859            
860            // Delivery receipt : Return-Receipt-To
861            if (_deliveryReceipt)
862            {
863                message.setHeader("Return-Receipt-To", _sender);
864            }
865            
866            // Read receipt : Disposition-Notification-To
867            if (_readReceipt)
868            {
869                message.setHeader("Disposition-Notification-To", _sender);
870            }
871            
872            Transport tr = session.getTransport("smtp");
873            
874            try
875            {
876                tr.connect(_host, (int) _port, StringUtils.trimToNull(_user), StringUtils.trimToNull(_password));
877                
878                _sendMail(message, tr);
879            }
880            finally
881            {
882                tr.close();
883            }
884        }
885        
886        private void _sendMail(MimeMessage message, Transport tr) throws MessagingException
887        {
888            if (_recipients != null && _recipients.size() > 0 && _sender != null)
889            {
890                if (_singleEmail)
891                {
892                    // Send one mail to multiple recipients
893                    message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(StringUtils.join(_recipients, ','), false));
894                    message.saveChanges();
895                    
896                    tr.sendMessage(message, message.getAllRecipients());
897                }
898                else
899                {
900                    // Send mail for each recipients
901                    for (String recipient : _recipients)
902                    {
903                        try
904                        {
905                            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipient, false));
906                            message.saveChanges();
907                            tr.sendMessage(message, message.getAllRecipients());
908                        }
909                        catch (Exception e) 
910                        {
911                            _LOGGER.error("Can't send mail with subject \"{}\" to the address {}", _subject, recipient, e);
912                        }
913                    }
914                }
915            }
916        }
917    }
918    
919    private static class MailSenderThreadFactory implements ThreadFactory
920    {
921        private ThreadFactory _defaultThreadFactory;
922
923        public MailSenderThreadFactory()
924        {
925            _defaultThreadFactory = Executors.defaultThreadFactory();
926        }
927        
928        public Thread newThread(Runnable r)
929        {
930            Thread thread = _defaultThreadFactory.newThread(r);
931            thread.setName("mail-sender-thread");
932            thread.setDaemon(true);
933            
934            return thread;
935        }
936    }
937}