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.io.InputStream; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Date; 025import java.util.HashMap; 026import java.util.LinkedList; 027import java.util.List; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.Optional; 031import java.util.Properties; 032import java.util.StringTokenizer; 033import java.util.concurrent.ExecutorService; 034import java.util.concurrent.Executors; 035import java.util.concurrent.ThreadFactory; 036import java.util.regex.Matcher; 037import java.util.regex.Pattern; 038 039import org.apache.avalon.framework.activity.Disposable; 040import org.apache.commons.lang3.StringUtils; 041import org.apache.commons.lang3.tuple.Pair; 042import org.jsoup.Jsoup; 043import org.jsoup.nodes.Document; 044import org.jsoup.nodes.Element; 045import org.jsoup.select.Elements; 046import org.jsoup.select.Selector; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049 050import org.ametys.runtime.config.Config; 051 052import jakarta.activation.DataHandler; 053import jakarta.mail.Message; 054import jakarta.mail.MessagingException; 055import jakarta.mail.Multipart; 056import jakarta.mail.Part; 057import jakarta.mail.Session; 058import jakarta.mail.Transport; 059import jakarta.mail.internet.InternetAddress; 060import jakarta.mail.internet.MimeBodyPart; 061import jakarta.mail.internet.MimeMessage; 062import jakarta.mail.internet.MimeMultipart; 063import jakarta.mail.internet.MimePart; 064import jakarta.mail.internet.PreencodedMimeBodyPart; 065 066/** 067 * Helper for sending mail 068 */ 069public final class SendMailHelper implements Disposable 070{ 071 /** Regexp to validate an email */ 072 public static final String EMAIL_VALIDATION_REGEXP = "^.+@.+$"; // No you cannot do better 073 /** Regexp to validate an email */ 074 public static final Pattern EMAIL_VALIDATION = Pattern.compile(EMAIL_VALIDATION_REGEXP); 075 076 private static final Logger __LOGGER = LoggerFactory.getLogger(SendMailHelper.class); 077 private static final ExecutorService __SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor(new MailSenderThreadFactory()); 078 079 private SendMailHelper () 080 { 081 // Nothing 082 } 083 084 @Override 085 public void dispose() 086 { 087 __SINGLE_THREAD_EXECUTOR.shutdownNow(); 088 } 089 090 private static class CssRule implements Comparable<CssRule> 091 { 092 private String _selector; 093 private String _properties; 094 private CssSpecificity _specificity; 095 096 /** 097 * CSSRule constructor 098 * @param selector css selector 099 * @param properties css properties for this rule 100 * @param positionIdx The rules declaration index 101 */ 102 public CssRule(String selector, String properties, int positionIdx) 103 { 104 _selector = selector; 105 _properties = properties; 106 _specificity = new CssSpecificity(_selector, positionIdx); 107 } 108 109 /** 110 * Selector getter 111 * @return the selector 112 */ 113 public String getSelector() 114 { 115 return _selector; 116 } 117 118 /** 119 * Properties getter 120 * @return the properties 121 */ 122 public String getProperties() 123 { 124 return _properties; 125 } 126 127 public int compareTo(CssRule r) 128 { 129 return _specificity.compareTo(r._specificity); 130 } 131 } 132 133 private static class CssSpecificity implements Comparable<CssSpecificity> 134 { 135 /** Attribute selectors pattern for CSS specificity processing */ 136 private static final Pattern __CSS_SPECIFICITY_ATTR_PATTERN = Pattern.compile("(\\[[^\\]]+\\])"); 137 /** ID selectors pattern for CSS specificity processing */ 138 private static final Pattern __CSS_SPECIFICITY_ID_PATTERN = Pattern.compile("(#[^\\s\\+>~\\.\\[:]+)"); 139 /** Class selectors pattern for CSS specificity processing */ 140 private static final Pattern __CSS_SPECIFICITY_CLASS_PATTERN = Pattern.compile("(\\.[^\\s\\+>~\\.\\[:]+)"); 141 /** Pseudo-element selectors pattern for CSS specificity processing */ 142 private static final Pattern __CSS_SPECIFICITY_PSEUDO_ELEMENT_PATTERN = Pattern.compile("(::[^\\s\\+>~\\.\\[:]+|:first-line|:first-letter|:before|:after)", Pattern.CASE_INSENSITIVE); 143 /** Pseudo-class (with bracket) selectors pattern for CSS specificity processing */ 144 private static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_WITH_BRACKETS_PATTERN = Pattern.compile("(:[\\w-]+\\([^\\)]*\\))", Pattern.CASE_INSENSITIVE); 145 /** Pseudo-class selectors pattern for CSS specificity processing */ 146 private static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_PATTERN = Pattern.compile("(:[^\\s\\+>~\\.\\[:]+)"); 147 /** Element selectors pattern for CSS specificity processing */ 148 private static final Pattern __CSS_SPECIFICITY_ELEMENT_PATTERN = Pattern.compile("([^\\s\\+>~\\.\\[:]+)"); 149 150 /** Specific :not pseudo-class selectors pattern for CSS specificity processing */ 151 private static final Pattern __CSS_SPECIFICITY_PSEUDO_CLASS_NOT_PATTERN = Pattern.compile(":not\\(([^\\)]*)\\)"); 152 /** Universal and separator characters pattern for CSS specificity processing */ 153 private static final Pattern __CSS_SPECIFICITY_UNIVERSAL_AND_SEPARATOR_PATTERN = Pattern.compile("[\\*\\s\\+>~]"); 154 155 156 private int[] _weights; 157 158 public CssSpecificity(String selector, int positionIdx) 159 { 160 // Position index is used to differentiate equality cases 161 // -> latest declaration should be the one applied 162 _weights = new int[]{0, 0, 0, 0, positionIdx}; 163 164 String input = selector; 165 166 // This part is loosely based on https://github.com/keeganstreet/specificity 167 168 // Remove :not pseudo-class but leave its argument 169 input = __CSS_SPECIFICITY_PSEUDO_CLASS_NOT_PATTERN.matcher(input).replaceAll(" $1 "); 170 171 // The following regular expressions assume that selectors matching the preceding regular expressions have been removed 172 input = _countReplaceAll(__CSS_SPECIFICITY_ATTR_PATTERN, input, 2); 173 input = _countReplaceAll(__CSS_SPECIFICITY_ID_PATTERN, input, 1); 174 input = _countReplaceAll(__CSS_SPECIFICITY_CLASS_PATTERN, input, 2); 175 input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_ELEMENT_PATTERN, input, 3); 176 // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang() 177 input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_CLASS_WITH_BRACKETS_PATTERN, input, 2); 178 // A regex for other pseudo classes, which don't have brackets 179 input = _countReplaceAll(__CSS_SPECIFICITY_PSEUDO_CLASS_PATTERN, input, 2); 180 181 // Remove universal selector and separator characters 182 input = __CSS_SPECIFICITY_UNIVERSAL_AND_SEPARATOR_PATTERN.matcher(input).replaceAll(" "); 183 184 _countReplaceAll(__CSS_SPECIFICITY_ELEMENT_PATTERN, input, 3); 185 } 186 187 private String _countReplaceAll(Pattern pattern, String selector, int sIndex) 188 { 189 Matcher m = pattern.matcher(selector); 190 StringBuffer sb = new StringBuffer(); 191 192 while (m.find()) 193 { 194 // Increment desired weight counter 195 _weights[sIndex]++; 196 197 // Replace matched selector part with whitespace 198 m.appendReplacement(sb, " "); 199 } 200 201 m.appendTail(sb); 202 203 return sb.toString(); 204 } 205 206 public int compareTo(CssSpecificity o) 207 { 208 for (int i = 0; i < _weights.length; i++) 209 { 210 if (_weights[i] != o._weights[i]) 211 { 212 return _weights[i] - o._weights[i]; 213 } 214 } 215 216 return 0; 217 } 218 } 219 220 /** 221 * Creates a new mail builder. 222 * @return the newly created {@link MailBuilder}. 223 */ 224 public static MailBuilder newMail() 225 { 226 return new MailBuilder(__LOGGER, __SINGLE_THREAD_EXECUTOR); 227 } 228 229 /* 230 * 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> 231 */ 232 public static String inlineCSS(String html) 233 { 234 List<CssRule> rules = new LinkedList<>(); 235 236 Document doc = Jsoup.parse(html); 237 Elements els = doc.select("style"); 238 239 for (Element e : els) 240 { 241 String styleRules = e.getAllElements().get(0).data(); 242 styleRules = styleRules.replaceAll("\t|\r|\n", "").replaceAll("<!--", "").replaceAll("-->", ""); 243 244 styleRules = _removeComments(styleRules); 245 246 styleRules = styleRules.trim(); 247 248 StringTokenizer st = new StringTokenizer(styleRules, "{}"); 249 while (st.countTokens() > 1) 250 { 251 String selectors = st.nextToken(); 252 String properties = st.nextToken(); 253 254 String[] selector = selectors.split(","); 255 for (String s : selector) 256 { 257 if (StringUtils.isNotBlank(s)) 258 { 259 rules.add(new CssRule(s.trim(), properties, rules.size())); 260 } 261 } 262 } 263 e.remove(); 264 } 265 266 // Sort rules by specificity 267 Collections.sort(rules, Collections.reverseOrder()); 268 269 for (CssRule rule : rules) 270 { 271 try 272 { 273 Elements selectedElements = doc.select(rule.getSelector()); 274 for (Element selElem : selectedElements) 275 { 276 String oldProperties = selElem.attr("style"); 277 selElem.attr("style", oldProperties.length() > 0 ? concatenateProperties(oldProperties, rule.getProperties()) : rule.getProperties()); 278 } 279 } 280 catch (Selector.SelectorParseException ex) 281 { 282 __LOGGER.error("Cannot inline CSS with rule \"" + rule.getSelector() + "\".Ignoring this rule and continuing.", ex); 283 } 284 } 285 286 return doc.toString(); 287 } 288 289 private static String _removeComments(String styleRules) 290 { 291 int i = styleRules.indexOf("/*"); 292 int j = styleRules.indexOf("*/"); 293 294 if (i >= 0 && j > i) 295 { 296 return styleRules.substring(0, i) + _removeComments(styleRules.substring(j + 2)); 297 } 298 299 return styleRules; 300 } 301 302 private static String concatenateProperties(String oldProp, String newProp) 303 { 304 String between = ""; 305 if (!newProp.endsWith(";")) 306 { 307 between += ";"; 308 } 309 310 return newProp + between + oldProp.trim(); // The existing (old) properties should take precedence. 311 } 312 313 /** 314 * Implements the builder pattern for creating and sending emails. 315 */ 316 public static class MailBuilder 317 { 318 private Logger _logger; 319 private ExecutorService _executor; 320 321 private boolean _async; 322 323 private String _subject; 324 private String _htmlBody; 325 private String _textBody; 326 private Collection<File> _attachments; 327 private Collection<NamedStream> _streamAttachments; 328 private List<String> _recipients; 329 private String _sender; 330 private List<String> _cc; 331 private List<String> _bcc; 332 private boolean _deliveryReceipt; 333 private boolean _readReceipt; 334 private String _host; 335 private long _port = -1; 336 private String _securityProtocol; 337 private String _user; 338 private String _password; 339 private boolean _singleEmail; 340 private List<String> _errorReport; 341 private boolean _inlineCSS = true; 342 private boolean _embedBase64Resources = true; 343 private Map<String, Pair<String, String>> _embeddedResource; 344 345 346 MailBuilder(Logger logger, ExecutorService executor) 347 { 348 _logger = logger; 349 _executor = executor; 350 } 351 352 /** 353 * If the mail should be sent asynchronously or not. 354 * @param async the asynchronous flag 355 * @return this builder 356 */ 357 public MailBuilder withAsync(boolean async) 358 { 359 _async = async; 360 return this; 361 } 362 363 /** 364 * Set the mail subject. This parameter is mandatory. 365 * @param subject the mail subject 366 * @return this builder 367 */ 368 public MailBuilder withSubject(String subject) 369 { 370 _subject = subject; 371 return this; 372 } 373 374 /** 375 * Set the optional HTML body. May be null. 376 * @param htmlBody the HTML mail body 377 * @return this builder 378 */ 379 public MailBuilder withHTMLBody(String htmlBody) 380 { 381 _htmlBody = htmlBody; 382 return this; 383 } 384 385 /** 386 * Set the optional HTML body. May be null. 387 * @param htmlBody the HTML mail body 388 * @param embeddedResource a map representing the resource related to the htmlBody. can be null. 389 * the map map is composed of the resource URI identified by the content ID used in the html body 390 * @return this builder 391 */ 392 public MailBuilder withHTMLBody(String htmlBody, Map<String, Pair<String, String>> embeddedResource) 393 { 394 _htmlBody = htmlBody; 395 _embeddedResource = embeddedResource; 396 return this; 397 } 398 399 /** 400 * Set the optional text body. May be null. 401 * @param textBody the text mail body 402 * @return this builder 403 */ 404 public MailBuilder withTextBody(String textBody) 405 { 406 _textBody = textBody; 407 return this; 408 } 409 410 /** 411 * Set the optional attachments. May be null. 412 * @param attachments the file attachments 413 * @return this builder 414 */ 415 public MailBuilder withAttachments(Collection<File> attachments) 416 { 417 _attachments = attachments; 418 return this; 419 } 420 421 /** 422 * Set the optional attachments. May be null. 423 * @param attachments the input stream attachments 424 * @return this builder 425 */ 426 public MailBuilder withAttachmentsAsStream(Collection<NamedStream> attachments) 427 { 428 _streamAttachments = attachments; 429 return this; 430 } 431 432 /** 433 * Set the mail recipient 434 * @param recipient the recipient address 435 * @return this builder 436 */ 437 public MailBuilder withRecipient(String recipient) 438 { 439 _recipients = List.of(recipient); 440 return this; 441 } 442 443 /** 444 * Set the mail recipients 445 * @param recipients the recipients addresses 446 * @return this builder 447 */ 448 public MailBuilder withRecipients(List<String> recipients) 449 { 450 _recipients = recipients; 451 return this; 452 } 453 454 /** 455 * Set the mail sender. 456 * @param sender the mail sender 457 * @return this builder 458 */ 459 public MailBuilder withSender(String sender) 460 { 461 _sender = sender; 462 return this; 463 } 464 465 /** 466 * Set the carbon copy address list. May be null. 467 * @param cc the cc address list 468 * @return this builder 469 */ 470 public MailBuilder withCc(List<String> cc) 471 { 472 _cc = cc; 473 return this; 474 } 475 476 /** 477 * Set the blind carbon copy address list. May be null. 478 * @param bcc the bcc address list 479 * @return this builder 480 */ 481 public MailBuilder withBcc(List<String> bcc) 482 { 483 _bcc = bcc; 484 return this; 485 } 486 487 /** 488 * Set to true to get a delivery receipt 489 * @param deliveryReceipt the delivery receipt flag 490 * @return this builder 491 */ 492 public MailBuilder withDeliveryReceipt(boolean deliveryReceipt) 493 { 494 _deliveryReceipt = deliveryReceipt; 495 return this; 496 } 497 498 /** 499 * Set to true to get a read receipt 500 * @param readReceipt the read receipt flag 501 * @return this builder 502 */ 503 public MailBuilder withReadReceipt(boolean readReceipt) 504 { 505 _readReceipt = readReceipt; 506 return this; 507 } 508 509 /** 510 * Set the SMTP host. Defaults to the value provided in the general configuration. 511 * @param host the SMTP host 512 * @return this builder 513 */ 514 public MailBuilder withHost(String host) 515 { 516 _host = host; 517 return this; 518 } 519 520 /** 521 * Set the SMTP port. Defaults to the value provided in the general configuration. 522 * @param port the SMTP port 523 * @return this builder 524 */ 525 public MailBuilder withPort(long port) 526 { 527 _port = port; 528 return this; 529 } 530 531 /** 532 * Set the security protocol ("starttls", "tlsssl"). May be null. 533 * @param securityProtocol the security protocol 534 * @return this builder 535 */ 536 public MailBuilder withSecurityProtocol(String securityProtocol) 537 { 538 _securityProtocol = securityProtocol; 539 return this; 540 } 541 542 /** 543 * Set the SMTP user. Defaults to the value provided in the general configuration. 544 * @param user the SMTP user 545 * @return this builder 546 */ 547 public MailBuilder withUser(String user) 548 { 549 _user = user; 550 return this; 551 } 552 553 /** 554 * Set the SMTP password. Defaults to the value provided in the general configuration. 555 * @param password the STMP password 556 * @return this builder 557 */ 558 public MailBuilder withPassword(String password) 559 { 560 _password = password; 561 return this; 562 } 563 564 /** 565 * Set to true if a single email with all recipients should be sent. Defaults to false. 566 * @param singleEmail the single email flag 567 * @return this builder 568 */ 569 public MailBuilder withSingleEmail(boolean singleEmail) 570 { 571 _singleEmail = singleEmail; 572 return this; 573 } 574 575 /** 576 * Set a list to get the send error report. The list will be populated with each recipient which the send failed. 577 * @param errorReport teh error report list to fill 578 * @return this builder 579 */ 580 public MailBuilder withErrorReport(List<String> errorReport) 581 { 582 _errorReport = errorReport; 583 return this; 584 } 585 586 /** 587 * Set to true to inline CSS rules. 588 * <br>Set to false to preserve provided HTML code as is. 589 * <br>Default to true for legacy purposes. 590 * @param inlineCSS the inlining CSS flag 591 * @return this builder 592 */ 593 public MailBuilder withInlineCSS(boolean inlineCSS) 594 { 595 _inlineCSS = inlineCSS; 596 return this; 597 } 598 599 /** 600 * Set to true to embed base64 resources as attachment. 601 * <br>Set to false to preserve provided HTML code as is. 602 * <br>Default to true. 603 * @param embedBase64Resources the embedding flag 604 * @return this builder 605 */ 606 public MailBuilder withEmbeddedBase64Resources(boolean embedBase64Resources) 607 { 608 _embedBase64Resources = embedBase64Resources; 609 return this; 610 } 611 612 /** 613 * Actually send mail. 614 * @throws MessagingException If an error occurred while preparing or sending email 615 * @throws IOException if an error occurs while attaching a file. 616 */ 617 public void sendMail() throws MessagingException, IOException 618 { 619 Optional<Config> config = Optional.ofNullable(Config.getInstance()); 620 621 MailSender mailSender = new MailSender(_logger, 622 _subject, 623 _textBody, 624 _prepareHTMLBody(_htmlBody), 625 _embeddedResource, 626 _attachments, 627 _streamAttachments, 628 _recipients, 629 StringUtils.defaultIfEmpty(_sender, config.map(c -> c.<String>getValue("smtp.mail.from")).orElse(null)), 630 _cc, 631 _bcc, 632 _deliveryReceipt, 633 _readReceipt, 634 StringUtils.defaultIfEmpty(_host, config.map(c -> c.<String>getValue("smtp.mail.host")).orElse(null)), 635 _port > 0 ? _port : config.map(c -> c.<Long>getValue("smtp.mail.port")).orElse(0L), 636 StringUtils.defaultIfEmpty(_securityProtocol, config.map(c -> c.<String>getValue("smtp.mail.security.protocol")).orElse(null)), 637 StringUtils.defaultIfEmpty(_user, config.map(c -> c.<String>getValue("smtp.mail.user")).orElse(null)), 638 StringUtils.defaultIfEmpty(_password, config.map(c -> c.<String>getValue("smtp.mail.password")).orElse(null)), _singleEmail, _errorReport); 639 640 if (!_async) 641 { 642 mailSender.sendMail(); 643 } 644 else 645 { 646 _executor.execute(mailSender); 647 } 648 } 649 650 private String _prepareHTMLBody(String htmlBody) 651 { 652 String result = htmlBody; 653 if (result != null) 654 { 655 if (_inlineCSS) 656 { 657 result = inlineCSS(result); 658 } 659 660 if (_embedBase64Resources) 661 { 662 result = _embedBase64Resources(result); 663 } 664 } 665 return result; 666 } 667 668 private String _embedBase64Resources(String html) 669 { 670 // parse the html to retrieve all base64 resources 671 // remove it from the html 672 // replace it with a cid 673 // add the base64 resource to the embeddedResource with it's cid 674 675 Pattern pattern = Pattern.compile("\"data:(\\w+/\\w+);base64,(\\S*)\""); 676 Matcher matcher = pattern.matcher(html); 677 678 if (_embeddedResource == null) 679 { 680 _embeddedResource = new HashMap<>(); 681 } 682 683 return matcher.replaceAll(match -> 684 { 685 String mimeType = match.group(1); 686 String base64Resource = match.group(2); 687 String hashCode = Integer.toString(base64Resource.hashCode()); 688 _embeddedResource.putIfAbsent(hashCode, Pair.of(mimeType, base64Resource)); 689 return "\"cid:" + hashCode + "\""; 690 }); 691 } 692 } 693 694 /** 695 * A record representing a stream with name and mime type 696 * 697 * @param inputStream the input stream 698 * @param name the name 699 * @param mimeType the mime type 700 */ 701 public record NamedStream(InputStream inputStream, String name, String mimeType) { /* */ } 702 703 private static class MailSender implements Runnable 704 { 705 private String _subject; 706 private String _textBody; 707 private String _htmlBody; 708 private Map<String, Pair<String, String>> _embeddedAttachments; 709 private Collection<File> _attachments; 710 private Collection<NamedStream> _streamAttachments; 711 private List<String> _recipients; 712 private String _sender; 713 private List<String> _cc; 714 private List<String> _bcc; 715 private boolean _deliveryReceipt; 716 private boolean _readReceipt; 717 private String _host; 718 private long _port; 719 private String _securityProtocol; 720 private String _user; 721 private String _password; 722 private boolean _singleEmail; 723 private List<String> _errorReport; 724 private Logger _logger; 725 726 /** 727 * Initialize the mail sender with email parameters 728 * @param logger The logger 729 * @param subject The mail subject 730 * @param textBody The text mail body. Can be null. 731 * @param htmlBody The HTML mail body. Can be null. 732 * @param embeddedResources a map representing the resources to embed. the key is the content id. the value is a pair of the mimeType and the base64 resource. 733 * @param attachments the file attachments. Can be null. 734 * @param streamAttachments the stream 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 port The server port 743 * @param securityProtocol the security protocol to use when transporting the email 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 * @param errorReport The error report list to get the recipients that failed. Can be null. 748 */ 749 public MailSender(Logger logger, String subject, String textBody, String htmlBody, Map<String, Pair<String, String>> embeddedResources, Collection<File> attachments, Collection<NamedStream> streamAttachments, 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, List<String> errorReport) 750 { 751 _logger = logger; 752 _subject = subject; 753 _textBody = textBody; 754 _htmlBody = htmlBody; 755 _embeddedAttachments = embeddedResources; 756 _attachments = attachments; 757 _streamAttachments = streamAttachments; 758 _recipients = recipients; 759 _sender = sender; 760 _cc = cc; 761 _bcc = bcc; 762 _deliveryReceipt = deliveryReceipt; 763 _readReceipt = readReceipt; 764 _host = host; 765 _port = port; 766 _securityProtocol = securityProtocol; 767 _user = user; 768 _password = password; 769 _singleEmail = singleEmail; 770 _errorReport = errorReport; 771 } 772 773 public void run() 774 { 775 try 776 { 777 sendMail(); 778 } 779 catch (Exception e) 780 { 781 _logger.error("Unable to send mail with subject: {}", _subject, e); 782 } 783 } 784 785 public void sendMail() throws MessagingException, IOException 786 { 787 List<InputStreamDataSource> dataSources = new ArrayList<>(); 788 try 789 { 790 Properties props = new Properties(); 791 792 // Setup mail server 793 props.put("mail.smtp.host", _host); 794 props.put("mail.smtp.port", _port); 795 796 // Security protocol 797 if (_securityProtocol.equals("starttls")) 798 { 799 props.put("mail.smtp.starttls.enable", "true"); 800 } 801 else if (_securityProtocol.equals("tlsssl")) 802 { 803 props.put("mail.smtp.ssl.enable", "true"); 804 } 805 806 Session session = Session.getInstance(props, null); 807 808 // Define message 809 MimeMessage message = new MimeMessage(session); 810 811 if (_sender != null) 812 { 813 message.setFrom(new InternetAddress(_sender)); 814 } 815 816 message.setSentDate(new Date()); 817 message.setSubject(_subject, "UTF-8"); 818 819 if (_attachments == null && _streamAttachments == null) 820 { 821 _setBodyPart(message); 822 } 823 else 824 { 825 Multipart multipart = new MimeMultipart("mixed"); 826 827 MimeBodyPart bodyPart = new MimeBodyPart(); 828 _setBodyPart(bodyPart); 829 multipart.addBodyPart(bodyPart); 830 831 if (_attachments != null) 832 { 833 for (File attachment : _attachments) 834 { 835 MimeBodyPart fileBodyPart = new MimeBodyPart(); 836 fileBodyPart.attachFile(attachment); 837 multipart.addBodyPart(fileBodyPart); 838 } 839 } 840 841 if (_streamAttachments != null) 842 { 843 for (NamedStream attachment : _streamAttachments) 844 { 845 MimeBodyPart fileBodyPart = new MimeBodyPart(); 846 InputStreamDataSource fds = new InputStreamDataSource(attachment); 847 dataSources.add(fds); 848 fileBodyPart.setDataHandler(new DataHandler(fds)); 849 fileBodyPart.setFileName(attachment.name()); 850 fileBodyPart.setDisposition(Part.ATTACHMENT); 851 multipart.addBodyPart(fileBodyPart); 852 } 853 } 854 855 message.setContent(multipart); 856 } 857 858 // Carbon copies 859 if (_cc != null) 860 { 861 message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(StringUtils.join(_cc, ','), false)); 862 } 863 864 // Blind carbon copies 865 if (_bcc != null) 866 { 867 message.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(StringUtils.join(_bcc, ','), false)); 868 } 869 870 // Delivery receipt : Return-Receipt-To 871 if (_deliveryReceipt) 872 { 873 message.setHeader("Return-Receipt-To", _sender); 874 } 875 876 // Read receipt : Disposition-Notification-To 877 if (_readReceipt) 878 { 879 message.setHeader("Disposition-Notification-To", _sender); 880 } 881 882 try (Transport tr = session.getTransport("smtp")) 883 { 884 tr.connect(_host, (int) _port, StringUtils.trimToNull(_user), StringUtils.trimToNull(_password)); 885 886 _sendMail(message, tr); 887 } 888 } 889 finally 890 { 891 for (InputStreamDataSource dataSource : dataSources) 892 { 893 dataSource.dispose(); 894 } 895 } 896 } 897 898 private void _setBodyPart(MimePart part) throws MessagingException 899 { 900 if (_textBody != null && _htmlBody != null) 901 { 902 Multipart bodyMultipart = new MimeMultipart("alternative"); 903 904 MimeBodyPart textPart = new MimeBodyPart(); 905 bodyMultipart.addBodyPart(textPart); 906 _setTextBody(textPart); 907 908 MimeBodyPart htmlPart = new MimeBodyPart(); 909 bodyMultipart.addBodyPart(htmlPart); 910 _setHTMLBody(htmlPart); 911 912 part.setContent(bodyMultipart); 913 } 914 else if (_textBody != null) 915 { 916 _setTextBody(part); 917 } 918 else if (_htmlBody != null) 919 { 920 _setHTMLBody(part); 921 } 922 } 923 924 private void _setTextBody(MimePart part) throws MessagingException 925 { 926 part.setText(_textBody, "UTF-8"); 927 } 928 929 private void _setHTMLBody(MimePart part) throws MessagingException 930 { 931 if (_embeddedAttachments != null && _embeddedAttachments.size() > 0) 932 { 933 // create a new multipart related to store the html and the attachments 934 MimeMultipart relatedMultipart = new MimeMultipart("related"); 935 part.setContent(relatedMultipart); 936 937 MimeBodyPart htmlPart = new MimeBodyPart(); 938 939 relatedMultipart.addBodyPart(htmlPart); 940 htmlPart.setText(_htmlBody, "UTF-8", "html"); 941 942 for (Entry<String, Pair<String, String>> entry : _embeddedAttachments.entrySet()) 943 { 944 MimeBodyPart entryPart = new PreencodedMimeBodyPart("base64"); 945 // < and > for Gmail support 946 entryPart.setContentID("<" + entry.getKey() + ">"); 947 entryPart.setDisposition(Part.INLINE); 948 entryPart.setFileName(entry.getKey()); 949 entryPart.setContent(entry.getValue().getRight(), entry.getValue().getLeft()); 950 951 relatedMultipart.addBodyPart(entryPart); 952 } 953 } 954 else 955 { 956 part.setText(_htmlBody, "UTF-8", "html"); 957 } 958 } 959 960 private void _sendMail(MimeMessage message, Transport tr) throws MessagingException 961 { 962 if (_recipients != null && _recipients.size() > 0 && _sender != null) 963 { 964 if (_singleEmail) 965 { 966 // Send one mail to multiple recipients 967 message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(StringUtils.join(_recipients, ','), false)); 968 message.saveChanges(); 969 970 tr.sendMessage(message, message.getAllRecipients()); 971 } 972 else 973 { 974 // Send mail for each recipients 975 for (String recipient : _recipients) 976 { 977 try 978 { 979 message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipient, false)); 980 message.saveChanges(); 981 tr.sendMessage(message, message.getAllRecipients()); 982 } 983 catch (Exception e) 984 { 985 if (_errorReport != null) 986 { 987 _errorReport.add(recipient); 988 } 989 _logger.error("Can't send mail with subject \"{}\" to the address {}", _subject, recipient, e); 990 } 991 } 992 } 993 } 994 } 995 } 996 997 private static class MailSenderThreadFactory implements ThreadFactory 998 { 999 private ThreadFactory _defaultThreadFactory; 1000 1001 public MailSenderThreadFactory() 1002 { 1003 _defaultThreadFactory = Executors.defaultThreadFactory(); 1004 } 1005 1006 public Thread newThread(Runnable r) 1007 { 1008 Thread thread = _defaultThreadFactory.newThread(r); 1009 thread.setName("mail-sender-thread"); 1010 thread.setDaemon(true); 1011 1012 return thread; 1013 } 1014 } 1015}