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