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