001/* 002 * Copyright 2022 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.plugins.forms.helper; 017 018import java.io.IOException; 019import java.io.InputStreamReader; 020import java.io.Reader; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Optional; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.components.ContextHelper; 038import org.apache.cocoon.environment.Request; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang.StringUtils; 041import org.apache.commons.lang3.ArrayUtils; 042import org.apache.excalibur.source.Source; 043import org.apache.excalibur.source.SourceResolver; 044 045import org.ametys.cms.data.File; 046import org.ametys.core.ui.Callable; 047import org.ametys.core.ui.mail.StandardMailBodyHelper; 048import org.ametys.core.user.User; 049import org.ametys.core.user.UserIdentity; 050import org.ametys.core.user.UserManager; 051import org.ametys.core.util.I18nUtils; 052import org.ametys.core.util.mail.SendMailHelper; 053import org.ametys.core.util.mail.SendMailHelper.MailBuilder; 054import org.ametys.core.util.mail.SendMailHelper.NamedStream; 055import org.ametys.plugins.forms.dao.FormDAO; 056import org.ametys.plugins.forms.question.FormQuestionType; 057import org.ametys.plugins.forms.question.sources.AbstractSourceType; 058import org.ametys.plugins.forms.question.sources.ManualWithEmailSourceType; 059import org.ametys.plugins.forms.question.sources.UsersSourceType; 060import org.ametys.plugins.forms.question.types.ChoicesListQuestionType; 061import org.ametys.plugins.forms.question.types.FileQuestionType; 062import org.ametys.plugins.forms.question.types.SimpleTextQuestionType; 063import org.ametys.plugins.forms.repository.Form; 064import org.ametys.plugins.forms.repository.FormEntry; 065import org.ametys.plugins.forms.repository.FormQuestion; 066import org.ametys.plugins.repository.AmetysObjectResolver; 067import org.ametys.runtime.config.Config; 068import org.ametys.runtime.i18n.I18nizableText; 069import org.ametys.runtime.plugin.component.AbstractLogEnabled; 070import org.ametys.web.repository.page.Page; 071import org.ametys.web.repository.site.Site; 072 073import jakarta.mail.MessagingException; 074 075/** 076 * The helper for form mail dialog 077 */ 078public class FormMailHelper extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 079{ 080 081 /** Avalon ROLE. */ 082 public static final String ROLE = FormMailHelper.class.getName(); 083 084 /** The param key for read restriction enable */ 085 public static final String READ_RESTRICTION_ENABLE = "read-restriction-enable"; 086 087 /** The value for entry user in the receiver combobox */ 088 public static final String RECEIVER_COMBOBOX_ENTRY_USER_VALUE = "entry-user"; 089 090 /** The empty value in the receiver combobox */ 091 public static final String RECEIVER_COMBOBOX_INPUT_ONLY = "input-only"; 092 093 /** The request key to ignore right */ 094 public static final String IGNORE_RIGHT_KEY = "ignore-right"; 095 096 /** Pattern for adding entry in acknowledgement of receipt if present in body */ 097 protected static final String _FORM_ENTRY_PATTERN = "{form}"; 098 099 /** Ametys object resolver. */ 100 protected AmetysObjectResolver _resolver; 101 102 /** I18n Utils */ 103 protected I18nUtils _i18nUtils; 104 105 /** The user manager */ 106 protected UserManager _userManager; 107 108 /** The source resolver. */ 109 protected SourceResolver _sourceResolver; 110 111 /** The context */ 112 protected Context _context; 113 114 /** The analyse of files for virus helper */ 115 protected FormDAO _formDAO; 116 117 /** The form admin mail helper */ 118 protected FormAdminMailsHelper _formAdminMailsHelper; 119 120 /** 121 * The type of limitation for the mail 122 */ 123 public enum LimitationMailType 124 { 125 /** The mail for queue */ 126 QUEUE, 127 /** The mail for the limit */ 128 LIMIT; 129 } 130 131 public void service(ServiceManager manager) throws ServiceException 132 { 133 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 134 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 135 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 136 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 137 _formDAO = (FormDAO) manager.lookup(FormDAO.ROLE); 138 _formAdminMailsHelper = (FormAdminMailsHelper) manager.lookup(FormAdminMailsHelper.ROLE); 139 } 140 141 public void contextualize(Context context) throws ContextException 142 { 143 _context = context; 144 } 145 146 /** 147 * Get the fields with email regex constraint so they can be used as receivers address for form admin mail 148 * @param formId Id of the form 149 * @return a map of the form question, key is the question id,value is the question title 150 */ 151 @Callable 152 public Map<String, Object> getAvailableAdminReceiverFields(String formId) 153 { 154 List<Object> receiverfields = new ArrayList<>(); 155 156 for (FormQuestion question : getQuestionWithMail(formId)) 157 { 158 Map<String, String> properties = new HashMap<>(); 159 properties.put("id", question.getNameForForm()); 160 properties.put("title", question.getTitle()); 161 receiverfields.add(properties); 162 } 163 164 return Map.of("data", receiverfields); 165 } 166 167 /** 168 * Get the fields with email regex constraint so they can be used as receivers address for form receipt mail 169 * @param formId Id of the form 170 * @return a map of the form question, key is the question id,value is the question title 171 */ 172 @SuppressWarnings("unchecked") 173 @Callable 174 public Map<String, Object> getAvailableReceiverFields(String formId) 175 { 176 Map<String, Object> results = getAvailableAdminReceiverFields(formId); 177 List<Object> receiverfields = (List<Object>) results.get("data"); 178 179 Map<String, String> properties = new HashMap<>(); 180 properties.put("id", RECEIVER_COMBOBOX_ENTRY_USER_VALUE); 181 properties.put("title", _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGINS_FORMS_ACKNOWLEDGEMENT_RECEIPT_ENTRY_USER"))); 182 receiverfields.add(properties); 183 184 return Map.of("data", receiverfields); 185 } 186 187 /** 188 * Get the list of questions which return a mail 189 * @param formId the form id 190 * @return the list of questions 191 */ 192 public List<FormQuestion> getQuestionWithMail(String formId) 193 { 194 List<FormQuestion> questions = new ArrayList<>(); 195 Form form = _resolver.resolveById(formId); 196 197 for (FormQuestion question : form.getQuestions()) 198 { 199 FormQuestionType type = question.getType(); 200 if (type instanceof SimpleTextQuestionType) 201 { 202 if (question.hasValue(SimpleTextQuestionType.ATTRIBUTE_REGEXP) && question.getValue(SimpleTextQuestionType.ATTRIBUTE_REGEXP).equals(SimpleTextQuestionType.EMAIL_REGEX_VALUE)) 203 { 204 questions.add(question); 205 } 206 } 207 else if (type instanceof ChoicesListQuestionType cLType && (cLType.getSourceType(question) instanceof UsersSourceType || cLType.getSourceType(question) instanceof ManualWithEmailSourceType)) 208 { 209 questions.add(question); 210 } 211 } 212 213 return questions; 214 } 215 216 /** 217 * Get all the email addresses from manual entry and/or a form field 218 * @param form the form 219 * @param entry the last entry 220 * @return an array of all the admin emails 221 */ 222 public String[] getAdminEmails(Form form, FormEntry entry) 223 { 224 Optional<String[]> adminEmails = form.getAdminEmails(); 225 Optional<String> otherAdminEmails = form.getOtherAdminEmails(); 226 String[] emailsAsArray = adminEmails.isPresent() ? adminEmails.get() : ArrayUtils.EMPTY_STRING_ARRAY; 227 Optional<String> otherEmailSource = getReceiver(entry, otherAdminEmails); 228 String[] mergedEmails = emailsAsArray; 229 if (otherEmailSource.isPresent()) 230 { 231 String otherEmails = otherEmailSource.get(); 232 String[] emails = otherEmails.split("[ ,;\r]"); 233 List<String> validEmails = _formAdminMailsHelper.getValidAdminEmails(emails); 234 if (!validEmails.isEmpty()) 235 { 236 mergedEmails = ArrayUtils.addAll(emailsAsArray, validEmails.toArray(new String[validEmails.size()])); 237 } 238 else 239 { 240 getLogger().error("Mails addresses " + otherEmails + " did not match regex"); 241 } 242 } 243 return mergedEmails; 244 } 245 246 /** 247 * Get the receiver from the entry 248 * @param entry the entry 249 * @param receiverPath the path to get receiver 250 * @return the receiver from the entry 251 */ 252 public Optional<String> getReceiver(FormEntry entry, Optional<String> receiverPath) 253 { 254 if (receiverPath.isEmpty()) 255 { 256 return Optional.empty(); 257 } 258 259 if (receiverPath.get().equals(RECEIVER_COMBOBOX_ENTRY_USER_VALUE)) 260 { 261 UserIdentity userIdentity = entry.getUser(); 262 User user = _userManager.getUser(userIdentity); 263 return Optional.ofNullable(user != null ? user.getEmail() : null); 264 } 265 else 266 { 267 FormQuestion question = entry.getForm().getQuestion(receiverPath.get()); 268 FormQuestionType type = question.getType(); 269 if (type instanceof SimpleTextQuestionType) 270 { 271 return Optional.ofNullable(entry.getValue(receiverPath.get())); 272 } 273 else if (type instanceof ChoicesListQuestionType cLType) 274 { 275 if (cLType.getSourceType(question) instanceof UsersSourceType && !entry.isMultiple(receiverPath.get())) 276 { 277 UserIdentity userIdentity = entry.getValue(receiverPath.get()); 278 if (userIdentity != null) 279 { 280 User user = _userManager.getUser(userIdentity); 281 return user != null ? Optional.ofNullable(user.getEmail()) : Optional.empty(); 282 } 283 } 284 else if (cLType.getSourceType(question) instanceof ManualWithEmailSourceType manualWithEmail) 285 { 286 String value = entry.getValue(receiverPath.get()); 287 Map<String, Object> enumParam = new HashMap<>(); 288 enumParam.put(AbstractSourceType.QUESTION_PARAM_KEY, question); 289 try 290 { 291 return Optional.ofNullable(manualWithEmail.getEntryEmail(value, enumParam)); 292 } 293 catch (Exception e) 294 { 295 getLogger().error("Could not get email from question '" + question.getNameForForm() + "' with value '" + value + "'"); 296 } 297 } 298 } 299 300 return Optional.empty(); 301 } 302 } 303 304 /** 305 * Send the notification emails. 306 * @param form the form. 307 * @param entry the user input. 308 * @param adminEmails list of email address where to send the notification 309 */ 310 public void sendEmailsForAdmin(Form form, FormEntry entry, String[] adminEmails) 311 { 312 try 313 { 314 String lang = _formDAO.getFormLocale(form); 315 String sender = Config.getInstance().getValue("smtp.mail.from"); 316 UserIdentity userIdentity = entry.getUser(); 317 User user = userIdentity != null ? _userManager.getUser(userIdentity) : null; 318 319 // Get subject 320 Optional<String> adminEmailSubject = form.getAdminEmailSubject(); 321 I18nizableText subject = adminEmailSubject.isPresent() 322 ? _replaceVariablesAndBreaks(form, user, adminEmailSubject.get(), lang) 323 : null; 324 325 // Get body with details 326 Optional<String> adminEmailBody = form.getAdminEmailBody(); 327 I18nizableText message = adminEmailBody.isPresent() 328 ? _replaceVariablesAndBreaks(form, user, adminEmailBody.get(), lang) 329 : null; 330 331 String entryDetails = getMail("entry.html", entry, Map.of(), false); 332 333 String prettyHtmlBody = StandardMailBodyHelper.newHTMLBody() 334 .withLanguage(lang) 335 .withTitle(subject) 336 .withMessage(message) 337 .withDetails(new I18nizableText("plugin.forms", "PLUGINS_FORMS_MAIL_RESULTS_DETAILS_TITLE"), entryDetails, false) 338 .build(); 339 340 // Get mail as text 341 String params = "?type=results&form-name=" + form.getTitle() + "&locale=" + lang; 342 String text = getMail("results.txt" + params, entry, Map.of(), false); 343 344 // Send mails 345 for (String email : adminEmails) 346 { 347 if (StringUtils.isNotEmpty(email)) 348 { 349 _sendMail(form, entry, _i18nUtils.translate(subject, lang), prettyHtmlBody, text, sender, email, false, false); 350 } 351 } 352 } 353 catch (IOException e) 354 { 355 getLogger().error("Error creating the notification message.", e); 356 } 357 } 358 359 private I18nizableText _replaceVariablesAndBreaks(Form form, User user, String mailText, String language) 360 { 361 String text = mailText; 362 text = StringUtils.replace(text, "{site}", form.getSite().getTitle()); 363 text = StringUtils.replace(text, "{user}", user != null ? user.getFullName() : _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGINS_FORMS_ADMIN_EMAILS_USER_ANONYMOUS"), language)); 364 text = StringUtils.replace(text, "{title}", form.getTitle()); 365 text = text.replaceAll("\r?\n", "<br/>"); 366 return new I18nizableText(text); 367 } 368 369 /** 370 * Send limitation mail when the limit is reached 371 * @param entry the form entry 372 * @param adminEmails list of email address where to send the notification 373 * @param limitationType the type of limitation 374 */ 375 public void sendLimitationReachedMailForAdmin(FormEntry entry, String[] adminEmails, LimitationMailType limitationType) 376 { 377 try 378 { 379 Form form = entry.getForm(); 380 381 String lang = _formDAO.getFormLocale(form); 382 String sender = Config.getInstance().getValue("smtp.mail.from"); 383 384 // Get subject 385 I18nizableText subject = limitationType == LimitationMailType.LIMIT 386 ? new I18nizableText("plugin.forms", "PLUGINS_FORMS_MAIL_LIMIT_SUBJECT", List.of(form.getTitle(), form.getSite().getTitle())) 387 : new I18nizableText("plugin.forms", "PLUGINS_FORMS_MAIL_QUEUE_SUBJECT", List.of(form.getTitle(), form.getSite().getTitle())); 388 389 // Get body 390 I18nizableText message = limitationType == LimitationMailType.LIMIT 391 ? new I18nizableText("plugin.forms", "PLUGINS_FORMS_MAIL_LIMIT_TEXT", List.of(form.getTitle())) 392 : new I18nizableText("plugin.forms", "PLUGINS_FORMS_MAIL_QUEUE_TEXT", List.of(form.getTitle())); 393 394 String prettyHtmlBody = StandardMailBodyHelper.newHTMLBody() 395 .withLanguage(lang) 396 .withTitle(subject) 397 .withMessage(message) 398 .build(); 399 400 // Get mail as text 401 I18nizableText textI18n = limitationType == LimitationMailType.LIMIT 402 ? new I18nizableText("plugin.forms", "PLUGINS_FORMS_MAIL_LIMIT_TEXT_NO_HTML", List.of(form.getTitle())) 403 : new I18nizableText("plugin.forms", "PLUGINS_FORMS_MAIL_QUEUE_TEXT_NO_HTML", List.of(form.getTitle())); 404 String text = _i18nUtils.translate(textI18n, lang); 405 406 for (String email : adminEmails) 407 { 408 if (StringUtils.isNotEmpty(email)) 409 { 410 _sendMail(form, entry, _i18nUtils.translate(subject, lang), prettyHtmlBody, text, sender, email, false, false); 411 } 412 } 413 } 414 catch (IOException e) 415 { 416 getLogger().error("Error creating the limit message.", e); 417 } 418 } 419 420 /** 421 * Send the receipt email. 422 * @param form the form. 423 * @param entry the current entry 424 */ 425 public void sendReceiptEmail(Form form, FormEntry entry) 426 { 427 if (form.getReceiptSender().isPresent()) 428 { 429 Optional<String> receiver = getReceiver(entry, form.getReceiptReceiver()); 430 if (receiver.isPresent()) 431 { 432 String lang = _formDAO.getFormLocale(form); 433 434 String sender = form.getReceiptSender().get(); 435 String subject = form.getReceiptSubject().get(); 436 String bodyTxt = form.getReceiptBody().get(); 437 String bodyHTML = bodyTxt.replaceAll("\r?\n", "<br/>"); 438 439 try 440 { 441 String prettyHtmlBody = StandardMailBodyHelper.newHTMLBody() 442 .withLanguage(lang) 443 .withTitle(subject) 444 .withMessage(bodyHTML) 445 .build(); 446 447 // Always check reading restriction for the receipt email 448 _sendMail(form, entry, subject, prettyHtmlBody, bodyTxt, sender, receiver.get(), true, true); 449 } 450 catch (IOException e) 451 { 452 getLogger().error("An error occurred sending receipt mail to '{}'", receiver.get(), e); 453 } 454 } 455 } 456 } 457 458 /** 459 * Send mail when the form entry if out of the queue 460 * @param form the form 461 * @param entry the form entry 462 */ 463 public void sendOutOfQueueMail(Form form, FormEntry entry) 464 { 465 Optional<String> receiver = getReceiver(entry, form.getQueueMailReceiver()); 466 if (receiver.isPresent()) 467 { 468 String lang = _formDAO.getFormLocale(form); 469 470 String sender = form.getQueueMailSender().get(); 471 String subject = form.getQueueMailSubject().get(); 472 String bodyTxt = form.getQueueMailBody().get(); 473 String bodyHTML = bodyTxt.replaceAll("\r?\n", "<br/>"); 474 475 try 476 { 477 String prettyHtmlBody = StandardMailBodyHelper.newHTMLBody() 478 .withLanguage(lang) 479 .withTitle(subject) 480 .withMessage(bodyHTML) 481 .build(); 482 483 // Always check reading restriction for the receipt email 484 _sendMail(form, entry, subject, prettyHtmlBody, bodyTxt, sender, receiver.get(), true, true); 485 } 486 catch (IOException e) 487 { 488 getLogger().error("An error occurred sending mail to get out of the queue to '{}'", receiver.get(), e); 489 } 490 } 491 } 492 493 /** 494 * Send invitation mails to the users 495 * @param form the form 496 * @param users the users to receive the mail 497 * @param message the invitation message 498 */ 499 public void sendInvitationMails(Form form, List<User> users, String message) 500 { 501 String lang = _formDAO.getFormLocale(form); 502 String sender = Config.getInstance().getValue("smtp.mail.from"); 503 504 // Get subject 505 I18nizableText subject = new I18nizableText("plugin.forms", "PLUGINS_FORMS_SEND_INVITATIONS_BOX_SUBJECT"); 506 507 // Get body 508 Site site = form.getSite(); 509 String formURI = _getFormURI(form, lang); 510 511 String replacedMessage = StringUtils.replace(message, "{site}", site.getTitle()); 512 String textMessage = StringUtils.replace(replacedMessage, "{link}", formURI); 513 String htmlMessage = StringUtils.replace(replacedMessage, "{link}", "<a href='" + formURI + "'>" + formURI + "</a>"); 514 515 for (User user : users) 516 { 517 try 518 { 519 String finalTextMessage = StringUtils.replace(textMessage, "{name}", user.getFullName()); 520 String finalHtmlMessage = StringUtils.replace(htmlMessage, "{name}", user.getFullName()); 521 String prettyHtmlBody = StandardMailBodyHelper.newHTMLBody() 522 .withLanguage(lang) 523 .withTitle(subject) 524 .withMessage(finalHtmlMessage) 525 .withLink(formURI, new I18nizableText("plugin.forms", "PLUGINS_FORMS_SEND_INVITATIONS_LINK_LABEL")) 526 .build(); 527 528 _sendMail(form, null, _i18nUtils.translate(subject, lang), prettyHtmlBody, finalTextMessage, sender, user.getEmail(), false, true); 529 } 530 catch (IOException e) 531 { 532 getLogger().error("Unable to send invitation mail to user {}", user.getEmail(), e); 533 } 534 } 535 } 536 537 /** 538 * Get the form page URI 539 * @param form the form 540 * @param language the language 541 * @return the form page URI 542 */ 543 protected String _getFormURI (Form form, String language) 544 { 545 Site site = form.getSite(); 546 Optional<Page> page = _formDAO.getFormPage(form.getId(), site.getName()) 547 .stream() 548 .filter(Page.class::isInstance) 549 .map(Page.class::cast) 550 .filter(p -> p.getSitemapName().equals(language)) 551 .findAny(); 552 553 return page.map(p -> site.getUrl() + "/" + p.getSitemap().getName() + "/" + p.getPathInSitemap() + ".html") 554 .orElse(null); 555 } 556 557 /** 558 * Get a mail pipeline's content. 559 * @param resource the mail resource pipeline 560 * @param entry the user input. 561 * @param additionalParameters the additional parameters 562 * @param withReadingRestriction <code>true</code> to enable reading restriction for form data in the mail 563 * @return the mail content. 564 * @throws IOException if an error occurs. 565 */ 566 public String getMail(String resource, FormEntry entry, Map<String, Object> additionalParameters, boolean withReadingRestriction) throws IOException 567 { 568 Source src = null; 569 Request request = ContextHelper.getRequest(_context); 570 Form form = entry.getForm(); 571 572 try 573 { 574 request.setAttribute(IGNORE_RIGHT_KEY, true); 575 576 String uri = "cocoon:/mail/entry/" + resource; 577 Map<String, Object> parameters = new HashMap<>(); 578 parameters.put("formId", form.getId()); 579 parameters.put("entryId", entry.getId()); 580 parameters.put(READ_RESTRICTION_ENABLE, withReadingRestriction); 581 582 parameters.putAll(additionalParameters); 583 584 src = _sourceResolver.resolveURI(uri, null, parameters); 585 Reader reader = new InputStreamReader(src.getInputStream(), "UTF-8"); 586 return IOUtils.toString(reader); 587 } 588 finally 589 { 590 request.setAttribute(IGNORE_RIGHT_KEY, false); 591 _sourceResolver.release(src); 592 } 593 } 594 595 /** 596 * Get the files of a user input. 597 * @param form The current form 598 * @param entry The current entry 599 * @return the files submitted by the user. 600 */ 601 protected Collection<NamedStream> _getFiles(Form form, FormEntry entry) 602 { 603 return form.getQuestions() 604 .stream() 605 .filter(q -> q.getType() instanceof FileQuestionType) 606 .map(q -> entry.<File>getValue(q.getNameForForm())) 607 .filter(Objects::nonNull) 608 .map(f -> new NamedStream(f.getInputStream(), f.getName(), f.getMimeType())) 609 .collect(Collectors.toList()); 610 } 611 612 private void _sendMail(Form form, FormEntry entry, String subject, String html, String text, String sender, String recipient, boolean addFormInformation, boolean withReadingRestriction) 613 { 614 try 615 { 616 String htmlBody = html; 617 String textBody = text; 618 619 Collection<NamedStream> records = entry != null ? _getFiles(form, entry) : new ArrayList<>(); 620 try 621 { 622 if (addFormInformation) 623 { 624 if (textBody.contains(_FORM_ENTRY_PATTERN)) 625 { 626 String entry2text = getMail("entry.txt", entry, Map.of(), withReadingRestriction); 627 textBody = StringUtils.replace(textBody, _FORM_ENTRY_PATTERN, entry2text); 628 } 629 630 if (htmlBody.contains(_FORM_ENTRY_PATTERN)) 631 { 632 String entry2html = getMail("entry.html", entry, Map.of(), withReadingRestriction); 633 htmlBody = StringUtils.replace(htmlBody, _FORM_ENTRY_PATTERN, entry2html); 634 } 635 } 636 637 MailBuilder mailBuilder = SendMailHelper.newMail() 638 .withAsync(true) 639 .withSubject(subject) 640 .withHTMLBody(htmlBody) 641 .withTextBody(textBody) 642 .withSender(sender) 643 .withRecipient(recipient); 644 645 if (!records.isEmpty()) 646 { 647 mailBuilder = mailBuilder.withAttachmentsAsStream(records); 648 } 649 mailBuilder.sendMail(); 650 } 651 finally 652 { 653 for (NamedStream record : records) 654 { 655 IOUtils.close(record.inputStream()); 656 } 657 } 658 } 659 catch (MessagingException | IOException e) 660 { 661 getLogger().error("Error sending the mail to " + recipient, e); 662 } 663 } 664}