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 <style> tags directly in the appropriates tags. e.g. : <style>h1 {color: red;}</style> <h1>a</h1> becomes <h1 style="color: red">a</h1> 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}