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