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