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        
363        sendMail(subject, htmlBody, textBody, attachments, recipients, sender, cc, bcc, deliveryReceipt, readReceipt, smtpHost, smtpPort, protocol, null, null, async);
364    }
365    
366    /**
367     * Sends mail without authentication, with attachments.
368     * @param subject The mail subject
369     * @param htmlBody The HTML mail body. Can be null.
370     * @param textBody The text mail body. Can be null.
371     * @param attachments the file attachments. Can be null.
372     * @param recipient The recipient address
373     * @param sender The sender address
374     * @param cc Carbon copy address list. Can be null.
375     * @param bcc Blind carbon copy address list. Can be null.
376     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
377     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
378     * @param host The server mail host
379     * @param port The server port
380     * @param securityProtocol The server mail security protocol
381     * @param async True to use asynchronous mail sending
382     * @throws MessagingException If an error occurred while preparing or sending email
383     * @throws IOException if an error occurs while attaching a file.
384     */
385    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
386    {
387        sendMail(subject, htmlBody, textBody, attachments, Collections.singletonList(recipient), sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, async);
388    }
389    
390    /**
391     * Sends mail without authentication, with attachments.
392     * @param subject The mail subject
393     * @param htmlBody The HTML mail body. Can be null.
394     * @param textBody The text mail body. Can be null.
395     * @param attachments the file attachments. Can be null.
396     * @param recipients The recipients addresses
397     * @param sender The sender address
398     * @param cc Carbon copy address list. Can be null.
399     * @param bcc Blind carbon copy address list. Can be null.
400     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
401     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
402     * @param host The server mail host
403     * @param port The server port
404     * @param securityProtocol The server mail security protocol
405     * @param async True to use asynchronous mail sending
406     * @throws MessagingException If an error occurred while preparing or sending email
407     * @throws IOException if an error occurs while attaching a file.
408     */
409    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
410    {
411        sendMail(subject, htmlBody, textBody, attachments, recipients, sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, null, null, async);
412    }
413    
414    /**
415     * Sends mail with authentication and attachments.
416     * @param subject The mail subject
417     * @param htmlBody The HTML mail body. Can be null.
418     * @param textBody The text mail body. Can be null.
419     * @param attachments the file attachments. Can be null.
420     * @param recipient The recipient address
421     * @param sender The sender address. Can be null when called by MailChecker.
422     * @param cc Carbon copy address list. Can be null.
423     * @param bcc Blind carbon copy address list. Can be null.
424     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
425     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
426     * @param host The server mail host. Can be null when called by MailChecker.
427     * @param securityProtocol the security protocol to use when transporting the email
428     * @param port The server port
429     * @param user The user name
430     * @param password The user password
431     * @param async True to use asynchronous mail sending
432     * @throws MessagingException If an error occurred while preparing or sending email
433     * @throws IOException if an error occurs while attaching a file.
434     */
435    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
436    {
437        sendMail(subject, htmlBody, textBody, attachments, Collections.singletonList(recipient), sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, user, password, async);
438    }
439
440    /**
441     * Sends mail with authentication and attachments.
442     * @param subject The mail subject
443     * @param htmlBody The HTML mail body. Can be null.
444     * @param textBody The text mail body. Can be null.
445     * @param attachments the file attachments. Can be null.
446     * @param recipients The recipients addresses
447     * @param sender The sender address. Can be null when called by MailChecker.
448     * @param cc Carbon copy address list. Can be null.
449     * @param bcc Blind carbon copy address list. Can be null.
450     * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
451     * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
452     * @param host The server mail host. Can be null when called by MailChecker.
453     * @param securityProtocol the security protocol to use when transporting the email
454     * @param port The server port
455     * @param user The user name
456     * @param password The user password
457     * @param async True to use asynchronous mail sending
458     * @throws MessagingException If an error occurred while preparing or sending email
459     * @throws IOException if an error occurs while attaching a file.
460     */
461    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
462    {
463        MailSender mailSender = new MailSender(_LOGGER, subject, htmlBody, textBody, attachments, recipients, sender, cc, bcc, deliveryReceipt, readReceipt, host, port, securityProtocol, user, password);
464        
465        if (!async)
466        {
467            mailSender.sendMail();
468        }
469        else
470        {
471            __SINGLE_THREAD_EXECUTOR.execute(mailSender);
472        }
473    }
474    
475    /**
476     * 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;
477     * @param html The initial non null html
478     * @return The inlined html
479     */
480    public static String inlineCSS(String html)
481    {
482        List<CssRule> rules = new LinkedList<>();
483        
484        Document doc = Jsoup.parse(html); 
485        Elements els = doc.select("style");
486
487        for (Element e : els) 
488        { 
489            String styleRules = e.getAllElements().get(0).data();
490            styleRules = styleRules.replaceAll("\t|\n", "").replaceAll("<!--", "").replaceAll("-->", "");
491
492            styleRules = _removeComments(styleRules);
493
494            styleRules = styleRules.trim();
495
496            StringTokenizer st = new StringTokenizer(styleRules, "{}"); 
497            while (st.countTokens() > 1) 
498            { 
499                String selectors = st.nextToken();
500                String properties = st.nextToken();
501
502                String[] selector = selectors.split(",");
503                for (String s : selector)
504                {
505                    if (StringUtils.isNotBlank(s))
506                    {
507                        rules.add(new CssRule(s, properties, rules.size()));
508                    }
509                }
510            } 
511            e.remove(); 
512        }
513        
514        // Sort rules by specificity
515        Collections.sort(rules, Collections.reverseOrder());
516    
517        for (CssRule rule : rules)
518        {
519            try
520            {
521                Elements selectedElements = doc.select(rule.getSelector());
522                for (Element selElem : selectedElements)
523                {
524                    String oldProperties = selElem.attr("style");
525                    selElem.attr("style", oldProperties.length() > 0 ? concatenateProperties(oldProperties, rule.getProperties()) : rule.getProperties());
526                }
527            }
528            catch (Selector.SelectorParseException ex)
529            {
530                _LOGGER.error("Cannot inline CSS. Ignoring this rule and continuing.", ex);
531            }
532        }
533
534        return doc.toString();
535    }
536
537    private static String _removeComments(String styleRules)
538    {
539        int i = styleRules.indexOf("/*");
540        int j = styleRules.indexOf("*/");
541
542        if (i >= 0 && j > i)
543        {
544            return styleRules.substring(0, i) + _removeComments(styleRules.substring(j + 2));
545        }
546
547        return styleRules;
548    }
549
550    private static String concatenateProperties(String oldProp, String newProp) 
551    { 
552        String between = "";
553        if (!newProp.endsWith(";"))
554        {
555            between += ";";
556        }
557        return newProp + between + oldProp.trim(); // The existing (old) properties should take precedence. 
558    } 
559    
560    @Override
561    public void dispose()
562    {
563        __SINGLE_THREAD_EXECUTOR.shutdownNow();
564    }
565    
566    private static class CssRule implements Comparable<CssRule>
567    {
568        private String _selector;
569        private String _properties;
570        private CssSpecificity _specificity;
571        
572        /**
573         * CSSRule constructor
574         * @param selector css selector
575         * @param properties css properties for this rule
576         * @param positionIdx The rules declaration index
577         */
578        public CssRule(String selector, String properties, int positionIdx)
579        {
580            _selector = selector;
581            _properties = properties;
582            _specificity = new CssSpecificity(_selector, positionIdx);
583        }
584        
585        /**
586         * Selector getter
587         * @return the selector
588         */
589        public String getSelector()
590        {
591            return _selector;
592        }
593        
594        /**
595         * Properties getter
596         * @return the properties
597         */
598        public String getProperties()
599        {
600            return _properties;
601        }
602        
603        public int compareTo(CssRule r)
604        {
605            return _specificity.compareTo(r._specificity);
606        }
607    }
608    
609    private static class CssSpecificity implements Comparable<CssSpecificity>
610    {
611        private int[] _weights;
612        
613        public CssSpecificity(String selector, int positionIdx)
614        {
615            // Position index is used to differentiate equality cases
616            // -> latest declaration should be the one applied
617            _weights = new int[]{0, 0, 0, 0, positionIdx};
618            
619            String input = selector;
620            
621            // This part is loosely based on https://github.com/keeganstreet/specificity
622            
623            // Remove :not pseudo-class but leave its argument
624            input = __CSS_SPECIFICITY_PSEUDO_CLASS_NOT_PATTERN.matcher(input).replaceAll(" $1 ");
625            
626            // The following regular expressions assume that selectors matching the preceding regular expressions have been removed
627            input = _countReplaceAll(__CSS_SPECIFICITY_ATTR_PATTERN, input, 2);
628            input = _countReplaceAll(__CSS_SPECIFICITY_ID_PATTERN, input, 1);
629            input = _countReplaceAll(__CSS_SPECIFICITY_CLASS_PATTERN, input, 2);
630            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_ELEMENT_PATTERN, input, 3);
631            // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang()
632            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_CLASS_WITH_BRACKETS_PATTERN, input, 2);
633            // A regex for other pseudo classes, which don't have brackets
634            input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_CLASS_PATTERN, input, 2);
635            
636            // Remove universal selector and separator characters
637            input = __CSS_SPECIFICITY_UNIVERSAL_AND_SEPARATOR_PATTERN.matcher(input).replaceAll(" ");
638            
639            _countReplaceAll(__CSS_SPECIFICITY_ELEMENT_PATTERN, input, 3);
640        }
641        
642        private String _countReplaceAll(Pattern pattern, String selector, int sIndex)
643        {
644            Matcher m = pattern.matcher(selector);
645            StringBuffer sb = new StringBuffer();
646            
647            while (m.find())
648            {
649                // Increment desired weight counter
650                _weights[sIndex]++;
651                
652                // Replace matched selector part with whitespace
653                m.appendReplacement(sb, " ");
654            }
655            
656            m.appendTail(sb);
657            
658            return sb.toString();
659        }
660        
661        public int compareTo(CssSpecificity o)
662        {
663            for (int i = 0; i < _weights.length; i++)
664            {
665                if (_weights[i] != o._weights[i])
666                {
667                    return _weights[i] - o._weights[i];
668                }
669            }
670            
671            return 0;
672        }
673    }
674    
675    private static class MailSender implements Runnable
676    {
677        private String _subject;
678        private String _htmlBody;
679        private String _textBody;
680        private Collection<File> _attachments;
681        private List<String> _recipients;
682        private String _sender;
683        private List<String> _cc;
684        private List<String> _bcc;
685        private boolean _deliveryReceipt;
686        private boolean _readReceipt;
687        private String _host;
688        private long _port;
689        private String _securityProtocol;
690        private String _user;
691        private String _password;
692        private Logger _logger;
693
694        /**
695         * Initialize the mail sender with email parameters
696         * @param logger The logger
697         * @param subject The mail subject
698         * @param htmlBody The HTML mail body. Can be null.
699         * @param textBody The text mail body. Can be null.
700         * @param attachments the file attachments. Can be null.
701         * @param recipients The recipients addresses
702         * @param sender The sender address. Can be null when called by MailChecker.
703         * @param cc Carbon copy address list. Can be null.
704         * @param bcc Blind carbon copy address list. Can be null.
705         * @param deliveryReceipt true to request that the receiving mail server send a notification when the mail is received.
706         * @param readReceipt true to request that the receiving mail client send a notification when the person opens the mail.
707         * @param host The server mail host. Can be null when called by MailChecker.
708         * @param securityProtocol the security protocol to use when transporting the email
709         * @param port The server port
710         * @param user The user name
711         * @param password The user password
712         */
713        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)
714        {
715            _logger = logger;
716            _subject = subject;
717            _htmlBody = htmlBody;
718            _textBody = textBody;
719            _attachments = attachments;
720            _recipients = recipients;
721            _sender = sender;
722            _cc = cc;
723            _bcc = bcc;
724            _deliveryReceipt = deliveryReceipt;
725            _readReceipt = readReceipt;
726            _host = host;
727            _port = port;
728            _securityProtocol = securityProtocol;
729            _user = user;
730            _password = password;
731        }
732        
733        public void run()
734        {
735            try
736            {
737                sendMail();
738            }
739            catch (Exception e)
740            {
741                _logger.error("Unable to send mail: " + _subject + "", e);
742            }
743        }
744        
745        public void sendMail() throws MessagingException, IOException
746        {
747            Properties props = new Properties();
748
749            // Setup mail server
750            props.put("mail.smtp.host", _host);
751            props.put("mail.smtp.port", _port);
752            
753            // Security protocol
754            if (_securityProtocol.equals("starttls"))
755            {
756                props.put("mail.smtp.starttls.enable", "true"); 
757            }
758            else if (_securityProtocol.equals("tlsssl"))
759            {
760                props.put("mail.smtp.ssl.enable", "true");
761            }
762            
763            Session session = Session.getInstance(props, null);
764            
765            // Define message
766            MimeMessage message = new MimeMessage(session);
767            
768            if (_sender != null)
769            {
770                message.setFrom(new InternetAddress(_sender));
771            }
772            
773            message.setSentDate(new Date());
774            message.setSubject(_subject);
775            
776            // Root multipart
777            Multipart multipart = new MimeMultipart("mixed");
778
779            // Message body part.
780            Multipart messageMultipart = new MimeMultipart("alternative");
781            MimeBodyPart messagePart = new MimeBodyPart();
782            messagePart.setContent(messageMultipart);
783            multipart.addBodyPart(messagePart);
784
785            if (_textBody != null)
786            {
787                MimeBodyPart textBodyPart = new MimeBodyPart();
788                textBodyPart.setContent(_textBody, "text/plain;charset=utf-8");
789                textBodyPart.addHeader("Content-Type", "text/plain;charset=utf-8");
790                messageMultipart.addBodyPart(textBodyPart);
791            }
792
793            if (_htmlBody != null)
794            {
795                MimeBodyPart htmlBodyPart = new MimeBodyPart();
796                htmlBodyPart.setContent(inlineCSS(_htmlBody), "text/html;charset=utf-8");
797                htmlBodyPart.addHeader("Content-Type", "text/html;charset=utf-8");
798                messageMultipart.addBodyPart(htmlBodyPart);
799            }
800
801            if (_attachments != null)
802            {
803                for (File attachment : _attachments)
804                {
805                    MimeBodyPart fileBodyPart = new MimeBodyPart();
806                    fileBodyPart.attachFile(attachment);
807                    multipart.addBodyPart(fileBodyPart);
808                }
809            }
810            message.setContent(multipart);
811
812            // Recipients
813            if (_recipients != null)
814            {
815                message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(StringUtils.join(_recipients, ','), false));
816            }
817            
818            // Carbon copies
819            if (_cc != null)
820            {
821                message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(StringUtils.join(_cc, ','), false));
822            }
823
824            // Blind carbon copies
825            if (_bcc != null)
826            {
827                message.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(StringUtils.join(_bcc, ','), false));
828            }
829            
830            // Delivery receipt : Return-Receipt-To
831            if (_deliveryReceipt)
832            {
833                message.setHeader("Return-Receipt-To", _sender);
834            }
835            
836            // Read receipt : Disposition-Notification-To
837            if (_readReceipt)
838            {
839                message.setHeader("Disposition-Notification-To", _sender);
840            }
841            
842            message.saveChanges();
843            
844            Transport tr = session.getTransport("smtp");
845            
846            try
847            {
848                tr.connect(_host, (int) _port, StringUtils.trimToNull(_user), StringUtils.trimToNull(_password));
849                
850                if (_recipients != null && _recipients.size() > 0 && _sender != null)
851                {
852                    tr.sendMessage(message, message.getAllRecipients());
853                }
854            }
855            finally
856            {
857                tr.close();
858            }
859        }
860    }
861    
862    private static class MailSenderThreadFactory implements ThreadFactory
863    {
864        private ThreadFactory _defaultThreadFactory;
865
866        public MailSenderThreadFactory()
867        {
868            _defaultThreadFactory = Executors.defaultThreadFactory();
869        }
870        
871        public Thread newThread(Runnable r)
872        {
873            Thread thread = _defaultThreadFactory.newThread(r);
874            thread.setName("mail-sender-thread");
875            thread.setDaemon(true);
876            
877            return thread;
878        }
879    }
880}