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