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.excalibur.source.Source;
042import org.apache.excalibur.source.SourceResolver;
043
044import org.ametys.cms.data.File;
045import org.ametys.core.ui.Callable;
046import org.ametys.core.user.User;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.core.user.UserManager;
049import org.ametys.core.util.I18nUtils;
050import org.ametys.core.util.mail.SendMailHelper;
051import org.ametys.core.util.mail.SendMailHelper.MailBuilder;
052import org.ametys.core.util.mail.SendMailHelper.NamedStream;
053import org.ametys.plugins.forms.dao.FormDAO;
054import org.ametys.plugins.forms.question.FormQuestionType;
055import org.ametys.plugins.forms.question.sources.UsersSourceType;
056import org.ametys.plugins.forms.question.types.ChoicesListQuestionType;
057import org.ametys.plugins.forms.question.types.FileQuestionType;
058import org.ametys.plugins.forms.question.types.SimpleTextQuestionType;
059import org.ametys.plugins.forms.repository.Form;
060import org.ametys.plugins.forms.repository.FormEntry;
061import org.ametys.plugins.forms.repository.FormQuestion;
062import org.ametys.plugins.repository.AmetysObjectResolver;
063import org.ametys.runtime.config.Config;
064import org.ametys.runtime.i18n.I18nizableText;
065import org.ametys.runtime.plugin.component.AbstractLogEnabled;
066
067import jakarta.mail.MessagingException;
068
069/**
070 * The helper for form mail dialog
071 */
072public class FormMailHelper extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
073{
074    /** Avalon ROLE. */
075    public static final String ROLE = FormMailHelper.class.getName();
076   
077    /** The param key for read restriction enable */
078    public static final String READ_RESTRICTION_ENABLE = "read-restriction-enable";
079    
080    /** The value for entry user in the receiver combobox */
081    public static final String RECEIVER_COMBOBOX_ENTRY_USER_VALUE = "entry-user";
082
083    /** The request key to ignore right */
084    public static final String IGNORE_RIGHT_KEY = "ignore-right";
085
086    /** Pattern for adding entry in acknowledgement of receipt if present in body */
087    protected static final String _FORM_ENTRY_PATTERN = "{form}";
088    
089    /** Ametys object resolver. */
090    protected AmetysObjectResolver _resolver;
091    
092    /** I18n Utils */
093    protected I18nUtils _i18nUtils;
094    
095    /** The user manager */
096    protected UserManager _userManager;
097    
098    /** The source resolver. */
099    protected SourceResolver _sourceResolver;
100    
101    /** The context */
102    protected Context _context;
103
104    /** The analyse of files for virus helper */
105    protected FormDAO _formDAO;
106
107    /**
108     * The type of limitation for the mail
109     */
110    public enum LimitationMailType 
111    {
112        /** The mail for queue */
113        QUEUE,
114        /** The mail for the limit */
115        LIMIT;
116    }
117    
118    public void service(ServiceManager manager) throws ServiceException
119    {
120        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
121        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
122        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
123        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
124        _formDAO = (FormDAO) manager.lookup(FormDAO.ROLE);
125    }
126    
127    public void contextualize(Context context) throws ContextException
128    {
129        _context = context;
130    }
131    
132    /**
133     * Get the fields with email regex constraint so they can be used as receivers address for form mail
134     * @param formId Id of the form
135     * @return a map of the form question, key is the question id,value is the question title 
136     */
137    @Callable
138    public Map<String, Object> getAvailableReceiverFields(String formId)
139    {
140        Map<String, Object> results = new HashMap<>();
141        List<Object> receiverfields = new ArrayList<>();
142        
143        for (FormQuestion question : getQuestionWithMail(formId))
144        {
145            Map<String, String> properties = new HashMap<>();
146            properties.put("id", question.getNameForForm());
147            properties.put("title", question.getTitle());
148            receiverfields.add(properties);
149        }
150        
151        Map<String, String> properties = new HashMap<>();
152        properties.put("id", RECEIVER_COMBOBOX_ENTRY_USER_VALUE);
153        properties.put("title", _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGINS_FORMS_ACKNOWLEDGEMENT_RECEIPT_ENTRY_USER")));
154        receiverfields.add(properties);
155        results.put("data", receiverfields);
156        
157        return results;
158    }
159    
160    /**
161     * Get the list of questions which return a mail
162     * @param formId the form id
163     * @return the list of questions
164     */
165    public List<FormQuestion> getQuestionWithMail(String formId)
166    {
167        List<FormQuestion> questions = new ArrayList<>();
168        Form form = _resolver.resolveById(formId);
169        
170        for (FormQuestion question : form.getQuestions())
171        {
172            FormQuestionType type = question.getType();
173            if (type instanceof SimpleTextQuestionType)
174            {
175                if (question.hasValue(SimpleTextQuestionType.ATTRIBUTE_REGEXP) && question.getValue(SimpleTextQuestionType.ATTRIBUTE_REGEXP).equals(SimpleTextQuestionType.EMAIL_REGEX_VALUE))
176                {
177                    questions.add(question);
178                }
179            }
180            else if (type instanceof ChoicesListQuestionType cLType && cLType.getSourceType(question) instanceof UsersSourceType)
181            {
182                questions.add(question);
183            }
184        }
185        
186        return questions;
187    }
188    
189    /**
190     * Get the receiver from the entry
191     * @param entry the entry
192     * @param receiverPath the path to get receiver
193     * @return the receiver from the entry
194     */
195    public Optional<String> getReceiver(FormEntry entry, Optional<String> receiverPath)
196    {
197        if (receiverPath.isEmpty())
198        {
199            return Optional.empty();
200        }
201        
202        if (receiverPath.get().equals(RECEIVER_COMBOBOX_ENTRY_USER_VALUE))
203        {
204            UserIdentity userIdentity = entry.getUser();
205            User user = _userManager.getUser(userIdentity);
206            return Optional.ofNullable(user != null ? user.getEmail() : null);
207        }
208        else
209        {
210            FormQuestion question = entry.getForm().getQuestion(receiverPath.get());
211            FormQuestionType type = question.getType();
212            if (type instanceof SimpleTextQuestionType)
213            {
214                return Optional.ofNullable(entry.getValue(receiverPath.get()));
215            }
216            else if (type instanceof ChoicesListQuestionType cLType && cLType.getSourceType(question) instanceof UsersSourceType)
217            {
218                if (!entry.isMultiple(receiverPath.get()))
219                {
220                    UserIdentity userIdentity = entry.getValue(receiverPath.get());
221                    if (userIdentity != null)
222                    {
223                        User user = _userManager.getUser(userIdentity);
224                        return user != null ? Optional.ofNullable(user.getEmail()) : Optional.empty();
225                    }
226                }
227            }
228            
229            return Optional.empty();
230        }
231    }
232    
233    /**
234     * Send the notification emails.
235     * @param form the form.
236     * @param entry the user input.
237     * @param adminEmails list of email address where to send the notification
238     */
239    public void sendEmailsForAdmin(Form form, FormEntry entry, String[] adminEmails)
240    {
241        try
242        {
243            String sender = Config.getInstance().getValue("smtp.mail.from");
244            String params = "?type=results&form-name=" + form.getTitle() + "&locale=" + _formDAO.getFormLocale(form);
245            String subjectParams = params;
246            UserIdentity userIdentity = entry.getUser();
247            if (userIdentity != null)
248            {
249                User user = _userManager.getUser(userIdentity);
250                if (user != null)
251                {
252                    subjectParams += "&user=" + user.getFullName();
253                }
254            }
255            String subject = getMail("subject.txt" + subjectParams, entry, Map.of(), false);
256            String html = getMail("results.html" + params, entry, Map.of(), false);
257            String text = getMail("results.txt" + params, entry, Map.of(), false);
258           
259            for (String email : adminEmails)
260            {
261                if (StringUtils.isNotEmpty(email))
262                {
263                    _sendMail(form, entry, subject, html, text, sender, email, false, false);
264                }
265            }
266        }
267        catch (IOException e)
268        {
269            getLogger().error("Error creating the notification message.", e);
270        }
271    }
272    
273    /**
274     * Send limitation mail when the limit is reached
275     * @param entry the form entry
276     * @param adminEmails list of email address where to send the notification
277     * @param limitationType the type of limitation
278     */
279    public void sendLimitationReachedMailForAdmin(FormEntry entry, String[] adminEmails, LimitationMailType limitationType)
280    {
281        try
282        {
283            Form form = entry.getForm();
284            
285            String sender = Config.getInstance().getValue("smtp.mail.from");
286            String type = limitationType.name().toLowerCase();
287            String params = "?type=" + type + "&form-name=" + form.getTitle() + "&locale=" + _formDAO.getFormLocale(form);
288            String subject = getMail("subject.txt" + params, entry, Map.of(), false);
289            String html = getMail(type + ".html" + params, entry, Map.of(), false);
290            String text = getMail(type + ".txt" + params, entry, Map.of(), false);
291            
292            for (String email : adminEmails)
293            {
294                if (StringUtils.isNotEmpty(email))
295                {
296                    _sendMail(form, entry, subject, html, text, sender, email, false, false);
297                }
298            }
299        }
300        catch (IOException e)
301        {
302            getLogger().error("Error creating the limit message.", e);
303        }
304    }
305    
306    /**
307     * Send the receipt email.
308     * @param form the form.
309     * @param entry the current entry
310     */
311    public void sendReceiptEmail(Form form, FormEntry entry)
312    {
313        if (form.getReceiptSender().isPresent())
314        {
315            Optional<String> receiver = getReceiver(entry, form.getReceiptReceiver());
316            if (receiver.isPresent())
317            {
318                String sender = form.getReceiptSender().get();
319                String subject = form.getReceiptSubject().get();
320                String bodyTxt = form.getReceiptBody().get();
321                String bodyHTML = bodyTxt.replaceAll("\r?\n", "<br/>");
322                
323                // Always check reading restriction for the receipt email
324                _sendMail(form, entry, subject, bodyHTML, bodyTxt, sender, receiver.get(), true, true);
325            }
326        }
327    }
328    
329    /**
330     * Send mail when the form entry if out of the queue
331     * @param form the form
332     * @param entry the form entry
333     */
334    public void sendOutOfQueueMail(Form form, FormEntry entry)
335    {
336        Optional<String> receiver = getReceiver(entry, form.getQueueMailReceiver());
337        if (receiver.isPresent())
338        {
339            String sender = form.getQueueMailSender().get();
340            String subject = form.getQueueMailSubject().get();
341            String bodyTxt = form.getQueueMailBody().get();
342            String bodyHTML = bodyTxt.replaceAll("\r?\n", "<br/>");
343            
344            // Always check reading restriction for the out of queue email
345            _sendMail(form, entry, subject, bodyHTML, bodyTxt, sender, receiver.get(), true, true);
346        }
347    }
348    
349    /**
350     * Get a mail pipeline's content.
351     * @param resource the mail resource pipeline 
352     * @param entry the user input.
353     * @param additionalParameters the additional parameters
354     * @param withReadingRestriction <code>true</code> to enable reading restriction for form data in the mail
355     * @return the mail content.
356     * @throws IOException if an error occurs.
357     */
358    public String getMail(String resource, FormEntry entry, Map<String, Object> additionalParameters, boolean withReadingRestriction) throws IOException
359    {
360        Source src = null;
361        Request request = ContextHelper.getRequest(_context);
362        Form form = entry.getForm();
363        
364        try
365        {
366            request.setAttribute(IGNORE_RIGHT_KEY, true);
367            
368            String uri = "cocoon:/mail/entry/" + resource;
369            Map<String, Object> parameters = new HashMap<>();
370            parameters.put("formId", form.getId());
371            parameters.put("entryId", entry.getId());
372            parameters.put(READ_RESTRICTION_ENABLE, withReadingRestriction);
373
374            parameters.putAll(additionalParameters);
375            
376            src = _sourceResolver.resolveURI(uri, null, parameters);
377            Reader reader = new InputStreamReader(src.getInputStream(), "UTF-8");
378            return IOUtils.toString(reader);
379        }
380        finally
381        {
382            request.setAttribute(IGNORE_RIGHT_KEY, false);
383            _sourceResolver.release(src);
384        }
385    }
386    
387    /**
388     * Get the files of a user input.
389     * @param form The current form
390     * @param entry The current entry
391     * @return the files submitted by the user.
392     */
393    protected Collection<NamedStream> _getFiles(Form form, FormEntry entry)
394    {
395        return form.getQuestions()
396            .stream()
397            .filter(q -> q.getType() instanceof FileQuestionType)
398            .map(q -> entry.<File>getValue(q.getNameForForm()))
399            .filter(Objects::nonNull)
400            .map(f -> new NamedStream(f.getInputStream(), f.getName(), f.getMimeType()))
401            .collect(Collectors.toList());
402    }
403    
404    private void _sendMail(Form form, FormEntry entry, String subject, String html, String text, String sender, String recipient, boolean addFormInformation, boolean withReadingRestriction)
405    {
406        try
407        {
408            String htmlBody = html;
409            String textBody = text;
410        
411            Collection<NamedStream> records = _getFiles(form, entry);
412            try
413            {
414                if (addFormInformation)
415                {
416                    if (textBody.contains(_FORM_ENTRY_PATTERN))
417                    {
418                        String entry2text = getMail("entry.txt", entry, Map.of(), withReadingRestriction);
419                        textBody = StringUtils.replace(textBody, _FORM_ENTRY_PATTERN, entry2text);
420                    }
421                    
422                    if (htmlBody.contains(_FORM_ENTRY_PATTERN))
423                    {
424                        String entry2html = getMail("entry.html", entry, Map.of(), withReadingRestriction);
425                        htmlBody = StringUtils.replace(htmlBody, _FORM_ENTRY_PATTERN, entry2html);
426                    }
427                }
428                
429                MailBuilder mailBuilder = SendMailHelper.newMail()
430                    .withAsync(true)
431                    .withSubject(subject)
432                    .withHTMLBody(htmlBody)
433                    .withTextBody(textBody)
434                    .withSender(sender)
435                    .withRecipient(recipient);
436                
437                if (!records.isEmpty())
438                {
439                    mailBuilder = mailBuilder.withAttachmentsAsStream(records);
440                }
441                mailBuilder.sendMail();
442            }
443            finally 
444            {
445                for (NamedStream record : records)
446                {
447                    IOUtils.close(record.inputStream());
448                }
449            }
450        }
451        catch (MessagingException | IOException e)
452        {
453            getLogger().error("Error sending the mail to " + recipient, e);
454        }
455    }
456}