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