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