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