001/*
002 *  Copyright 2010 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.cms.workflow;
017
018import java.io.IOException;
019import java.time.ZonedDateTime;
020import java.util.ArrayList;
021import java.util.HashSet;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import java.util.Objects;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.commons.lang.StringUtils;
036import org.apache.excalibur.source.SourceResolver;
037
038import org.ametys.cms.repository.WorkflowAwareContent;
039import org.ametys.core.right.Right;
040import org.ametys.core.right.RightManager;
041import org.ametys.core.right.RightsExtensionPoint;
042import org.ametys.core.ui.mail.StandardMailBodyHelper;
043import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder;
044import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder.UserInput;
045import org.ametys.core.user.User;
046import org.ametys.core.user.UserIdentity;
047import org.ametys.core.user.UserManager;
048import org.ametys.core.util.I18nUtils;
049import org.ametys.core.util.mail.SendMailHelper;
050import org.ametys.plugins.workflow.EnhancedFunction;
051import org.ametys.plugins.workflow.component.WorkflowArgument;
052import org.ametys.plugins.workflow.support.WorkflowElementDefinitionHelper;
053import org.ametys.plugins.workflow.support.WorkflowProvider;
054import org.ametys.runtime.config.Config;
055import org.ametys.runtime.i18n.I18nizableText;
056import org.ametys.runtime.i18n.I18nizableTextParameter;
057import org.ametys.runtime.model.StaticEnumerator;
058import org.ametys.runtime.plugin.component.PluginAware;
059
060import com.opensymphony.module.propertyset.PropertySet;
061import com.opensymphony.workflow.WorkflowException;
062
063import jakarta.mail.MessagingException;
064
065/**
066 * OS workflow function to send mail after an action is triggered.
067 */
068public class SendMailFunction extends AbstractContentWorkflowComponent implements EnhancedFunction, Initializable, PluginAware, Contextualizable
069{
070    /**
071     * Provide "false" to prevent the function sending the mail.
072     * Useful when making large automatic workflow operations (for instance, when bulk importing and proposing in one action). 
073     */
074    public static final String SEND_MAIL = "send-mail";
075    
076    /** The rights key. */
077    protected static final String RIGHTS_KEY = "rights";
078    /** The mail subject key. */
079    protected static final String SUBJECT_KEY = "subjectKey";
080    /** The mail body key. */
081    protected static final String BODY_KEY = "bodyKey";
082    
083    /** The rights manager. */
084    protected RightManager _rightManager;
085    
086    /** The users manager. */
087    protected UserManager _userManager;
088    
089    /** The source resolver. */
090    protected SourceResolver _sourceResolver;
091    
092    /** The workflow. */
093    protected WorkflowProvider _workflowProvider;
094    
095    /** The Avalon context. */
096    protected Context _context;
097    
098    /** The plugin name. */
099    protected String _pluginName;
100    
101    /** I18nUtils */
102    protected I18nUtils _i18nUtils;
103    
104    /** The rights extension point */ 
105    protected RightsExtensionPoint _rightsExtensionPoint;
106    
107    @Override
108    public void initialize() throws Exception
109    {
110        _rightManager = (RightManager) _manager.lookup(RightManager.ROLE);
111        _userManager = (UserManager) _manager.lookup(UserManager.ROLE);
112        _sourceResolver = (SourceResolver) _manager.lookup(SourceResolver.ROLE);
113        _workflowProvider = (WorkflowProvider) _manager.lookup(WorkflowProvider.ROLE);
114        _i18nUtils = (I18nUtils) _manager.lookup(I18nUtils.ROLE);
115        _rightsExtensionPoint = (RightsExtensionPoint) _manager.lookup(RightsExtensionPoint.ROLE);
116    }
117    
118    public void contextualize(Context context) throws ContextException
119    {
120        _context = context;
121    }
122    
123    @Override
124    public void setPluginInfo(String pluginName, String featureName, String id)
125    {
126        _pluginName = pluginName;
127    }
128    
129    @Override
130    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
131    {
132        String rightsParam = StringUtils.defaultString((String) args.get(RIGHTS_KEY));
133        String subjectI18nKey = StringUtils.defaultString((String) args.get(SUBJECT_KEY));
134        String bodyI18nKey = StringUtils.defaultString((String) args.get(BODY_KEY));
135        
136        Set<String> rights = _getRights(rightsParam);
137        
138        // If "send-mail" is set to true or is not present in the vars, send the mail.
139        boolean dontSendMail = "false".equals(transientVars.get(SEND_MAIL));
140        
141        if (dontSendMail)
142        {
143            return;
144        }
145        
146        try
147        {
148            WorkflowAwareContent content = getContent(transientVars);
149            
150            Set<String> recipients = getRecipients(transientVars, content, rights);
151            
152            if (recipients.size() > 0)
153            {
154                User caller = getCaller(transientVars, content);
155                String sender = getSender(transientVars, content);
156                String mailSubject = getMailSubject(subjectI18nKey, caller, content, transientVars);
157                String mailBody = getMailBody(subjectI18nKey, bodyI18nKey, caller, content, transientVars);
158                
159                _sendMails(mailSubject, mailBody, recipients, sender);
160            }
161        }
162        catch (Exception e)
163        {
164            _logger.error("An error occurred: unable to send mail to notify workflow change.", e);
165        }
166    }
167
168    private Set<String> _getRights(String rightsParam)
169    {
170        Set<String> rights = new HashSet<>();
171        for (String right : rightsParam.split(","))
172        {
173            if (StringUtils.isNotBlank(right))
174            {
175                rights.add(right.trim());
176            }
177        }
178        return rights;
179    }
180    
181    /**
182     * Get the subject of mail
183     * @param subjectI18nKey  the i18n key to use for subject
184     * @param user the caller
185     * @param content the content
186     * @param transientVars the transient variables
187     * @return the subject
188     */
189    protected String getMailSubject (String subjectI18nKey, User user, WorkflowAwareContent content, Map transientVars)
190    {
191        I18nizableText subjectKey = new I18nizableText(null, subjectI18nKey, getSubjectI18nParams(user, content));
192        return _i18nUtils.translate(subjectKey, null); // FIXME Use user preference language
193    }
194    
195    /**
196     * Get the text body of mail
197     * @param subjectI18nKey the i18n key to use for body's title
198     * @param bodyI18nKey the i18n key to use for body
199     * @param user the caller
200     * @param content the content
201     * @param transientVars the transient variables
202     * @return the text body
203     * @throws IOException if an error occurred while building HTML workflow email
204     */
205    protected String getMailBody (String subjectI18nKey, String bodyI18nKey, User user, WorkflowAwareContent content, Map transientVars) throws IOException
206    {
207        // FIXME Use user preference language
208        
209        MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody()
210            .withTitle(getMailSubject(subjectI18nKey, user, content, transientVars))
211            .withMessage(new I18nizableText(null, bodyI18nKey, getBodyI18nParams(user, content)))
212            .withLink(_getContentUri(content), new I18nizableText("plugin.cms", "WORKFLOW_MAIL_BODY_GO_TO_CONTENT"));
213        
214        // Get the workflow comment
215        String comment = (String) transientVars.get("comment");
216        if (StringUtils.isNotEmpty(comment))
217        {
218            bodyBuilder.withUserInput(new UserInput(user, ZonedDateTime.now(), comment), new I18nizableText("plugin.cms", "WORKFLOW_MAIL_BODY_USER_COMMENT"));
219        }
220        
221        return bodyBuilder.build();
222    }
223    
224    /**
225     * Send the notification emails.
226     * @param subject the e-mail subject.
227     * @param body the e-mail body.
228     * @param recipients the recipients emails address.
229     * @param from the address sending the e-mail.
230     */
231    protected void _sendMails(String subject, String body, Set<String> recipients, String from)
232    {
233        for (String recipient : recipients)
234        {
235            try
236            {
237                SendMailHelper.newMail()
238                              .withSubject(subject)
239                              .withHTMLBody(body)
240                              .withSender(from)
241                              .withRecipient(recipient)
242                              .withAsync(true)
243                              .sendMail();
244            }
245            catch (MessagingException | IOException e)
246            {
247                _logger.warn("Could not send a workflow notification mail to " + recipient, e);
248            }
249        }
250    }
251    
252    /**
253     * Get the i18n parameters of mail subject
254     * @param user the caller
255     * @param content the content
256     * @return the i18n parameters
257     */
258    protected List<String> getSubjectI18nParams (User user, WorkflowAwareContent content)
259    {
260        List<String> params = new ArrayList<>();
261        params.add(_contentHelper.getTitle(content));
262        return params;
263    }
264    
265    /**
266     * Get the i18n parameters of mail body text
267     * @param user the caller
268     * @param content the content
269     * @return the i18n parameters
270     */
271    protected List<String> getBodyI18nParams (User user, WorkflowAwareContent content)
272    {
273        List<String> params = new ArrayList<>();
274        
275        params.add(user.getFullName()); // {0}
276        params.add(content.getTitle()); // {1}
277        params.add(_getContentUri(content)); // {2}
278        
279        return params;
280    }
281    
282    /**
283     * Get the content uri
284     * @param content the content
285     * @return the content uri
286     */
287    protected String _getContentUri(WorkflowAwareContent content)
288    {
289        String requestUri = _getRequestUri();
290        return requestUri + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27";
291    }
292    
293    /**
294     * Get the request URI.
295     * @return the full request URI.
296     */
297    protected String _getRequestUri()
298    {
299        return StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
300    }
301    
302    /**
303     * Retrieve the request from which this component is called.
304     * @return the request or <code>null</code>.
305     */
306    public Request _getRequest()
307    {
308        try 
309        {
310            return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT);
311        } 
312        catch (ContextException ce)
313        {
314            _logger.info("Unable to get the request", ce);
315            return null;
316        }
317    }
318    
319    /**
320     * Get the caller of the workflow action 
321     * @param transientVars the transient variables
322     * @param content content the content
323     * @return caller the caller if the workflow function
324     * @throws WorkflowException if failed to get caller
325     */
326    public User getCaller(Map transientVars, WorkflowAwareContent content) throws WorkflowException
327    {
328        UserIdentity userIdentity = getUser(transientVars);
329        return userIdentity != null ? _userManager.getUser(userIdentity) : null;
330    }
331    
332    /**
333     * Get the sender for mail
334     * @param transientVars the transient variables
335     * @param content the content
336     * @return the sender email address
337     * @throws WorkflowException if failed to get email for sender
338     */
339    protected String getSender(Map transientVars, WorkflowAwareContent content) throws WorkflowException
340    {
341        User user = getCaller(transientVars, content);
342        return user != null ? user.getEmail() : null;
343    }
344    
345    /**
346     * Get the recipients
347     * @param transientVars the transient variables
348     * @param content the content.
349     * @param rights the set of rights to check.
350     * @return the recipients.
351     * @throws WorkflowException If failed to get recipients
352     */
353    protected Set<String> getRecipients(Map transientVars, WorkflowAwareContent content, Set<String> rights) throws WorkflowException
354    {
355        Set<UserIdentity> users = _getUsers(content, rights);
356        
357        return users.stream()
358            .map(_userManager::getUser)
359            .filter(Objects::nonNull)
360            .map(User::getEmail)
361            .filter(StringUtils::isNotBlank)
362            .collect(Collectors.toSet());
363    }
364    
365    /**
366     * Get the user logins.
367     * @param content the content.
368     * @param rights the set of rights to check.
369     * @return the users.
370     * @throws WorkflowException If an error occurred
371     */
372    protected Set<UserIdentity> _getUsers(WorkflowAwareContent content, Set<String> rights) throws WorkflowException
373    {
374        Set<UserIdentity> users = new HashSet<>();
375        
376        Iterator<String> rightIt = rights.iterator();
377        
378        // First right : add all the granted users.
379        if (rightIt.hasNext())
380        {
381            users.addAll(_rightManager.getAllowedUsers(rightIt.next(), content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
382        }
383        
384        // Next rights : retain all the granted users.
385        while (rightIt.hasNext())
386        {
387            users.retainAll(_rightManager.getAllowedUsers(rightIt.next(), content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
388        }
389        
390        return users;
391    }
392    
393    @Override
394    public FunctionType getFunctionExecType()
395    {
396        return FunctionType.POST;
397    }
398
399    @Override
400    public I18nizableText getLabel()
401    {
402        return new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_FUNCTION_LABEL");
403    }
404    
405    @SuppressWarnings("unchecked")
406    @Override
407    public List<WorkflowArgument> getArguments()
408    {
409        WorkflowArgument rights = WorkflowElementDefinitionHelper.getElementDefinition(
410                RIGHTS_KEY,
411                new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_RIGHTS_KEY_LABEL"),
412                new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_RIGHTS_KEY_DESCRIPTION"),
413                false,
414                true
415            );
416        StaticEnumerator<String> rightsStaticEnumerator = new StaticEnumerator<>();
417        for (String rightId : _rightsExtensionPoint.getExtensionsIds())
418        {
419            Right right = _rightsExtensionPoint.getExtension(rightId);
420            Map<String, I18nizableTextParameter> params = Map.of("category", right.getCategory(), "label", right.getLabel());
421            rightsStaticEnumerator.add(new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_EDITOR_CHECK_RIGHTS_ARGUMENT_RIGHT_KEY_PARAMS_LABEL", params), right.getId());
422        }
423        rights.setEnumerator(rightsStaticEnumerator);
424        
425        return List.of(
426            WorkflowElementDefinitionHelper.getElementDefinition(
427                SUBJECT_KEY,
428                new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_SUBJECT_KEY_LABEL"),
429                new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_SUBJECT_KEY_DESCRIPTION"),
430                true,
431                false
432            ), 
433            WorkflowElementDefinitionHelper.getElementDefinition(
434                BODY_KEY,
435                new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_BODY_KEY_LABEL"),
436                new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_BODY_KEY_DESCRIPTION"),
437                true,
438                false
439            ), 
440            rights
441        );
442    }
443
444    @Override
445    public I18nizableText getFullLabel(Map<String, String> argumentsValues)
446    {
447        String rightsParam = StringUtils.defaultString(argumentsValues.get(RIGHTS_KEY));
448        if (!rightsParam.isBlank())
449        {
450            Object[] rightsIds = _getRights(rightsParam).toArray();
451            Right right = _rightsExtensionPoint.getExtension((String) rightsIds[0]);
452            String concatenatedRights = "<strong>" + _i18nUtils.translate(right.getLabel()) + "</strong>";
453            String and = _i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_CONDITION_AND"));
454            for (int i = 1; i < rightsIds.length; i++)
455            {
456                right = _rightsExtensionPoint.getExtension((String) rightsIds[i]);
457                concatenatedRights += and + "<strong>" + _i18nUtils.translate(right.getLabel()) + "</strong>";
458            }
459            return new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_FUNCTION_RIGHTS_DESCRIPTION", List.of(concatenatedRights));
460        }
461        return getLabel();
462    }
463}