001/*
002 *  Copyright 2014 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.plugins.workspaces.project.notification;
017
018import java.io.ByteArrayOutputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.List;
024import java.util.Map;
025import java.util.Objects;
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.collections.CollectionUtils;
035import org.apache.commons.lang3.BooleanUtils;
036import org.apache.commons.lang3.StringUtils;
037import org.apache.excalibur.source.Source;
038import org.apache.excalibur.source.SourceResolver;
039import org.apache.excalibur.source.SourceUtil;
040
041import org.ametys.core.right.RightManager;
042import org.ametys.core.user.User;
043import org.ametys.core.user.UserIdentity;
044import org.ametys.core.user.UserManager;
045import org.ametys.core.util.I18nUtils;
046import org.ametys.core.util.mail.SendMailHelper;
047import org.ametys.plugins.repository.AmetysObject;
048import org.ametys.plugins.repository.AmetysObjectResolver;
049import org.ametys.plugins.workflow.support.WorkflowProvider;
050import org.ametys.plugins.workspaces.calendars.Calendar;
051import org.ametys.plugins.workspaces.calendars.events.CalendarEvent;
052import org.ametys.plugins.workspaces.project.ProjectManager;
053import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper;
054import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper.Frequency;
055import org.ametys.plugins.workspaces.project.objects.Project;
056import org.ametys.plugins.workspaces.workflow.AbstractNodeWorkflowComponent;
057import org.ametys.runtime.config.Config;
058import org.ametys.runtime.i18n.I18nizableText;
059import org.ametys.runtime.plugin.component.PluginAware;
060import org.ametys.web.WebConstants;
061import org.ametys.web.renderingcontext.RenderingContext;
062import org.ametys.web.renderingcontext.RenderingContextHandler;
063import org.ametys.web.repository.site.Site;
064
065import com.opensymphony.module.propertyset.PropertySet;
066import com.opensymphony.workflow.FunctionProvider;
067import com.opensymphony.workflow.WorkflowException;
068
069import jakarta.mail.MessagingException;
070
071/**
072 * OS workflow function to send mail after an action is triggered.
073 */
074public class SendCalendarNotificationFunction extends AbstractNodeWorkflowComponent implements FunctionProvider, Initializable, PluginAware, Contextualizable
075{
076    /**
077     * Provide "false" to prevent the function sending the mail.
078     * Useful when making large automatic workflow operations (for instance, when bulk importing and proposing in one action). 
079     */
080    public static final String SEND_MAIL = "send-mail";
081    
082    /** The mail subject key. */
083    protected static final String SUBJECT_KEY = "subjectKey";
084    
085    /** The mail body key. */
086    protected static final String BODY_KEY = "bodyKey";
087
088    private static final String RIGHTS = "rights";
089    
090    /** The right manager. */
091    protected RightManager _rightManager;
092    
093    /** The users manager. */
094    protected UserManager _userManager;
095    
096    /** The workflow provider */
097    protected WorkflowProvider _workflowProvider;
098    
099    /** The plugin name. */
100    protected String _pluginName;
101    
102    /** I18nUtils */
103    protected I18nUtils _i18nUtils;
104    
105    /** The ametys resolver */
106    protected AmetysObjectResolver _resolver;
107    
108    /** The project resolver */
109    protected ProjectManager _projectManager;
110    
111    /** Context available to subclasses. */
112    protected Context _context;
113    
114    /** The rendering context handler */
115    protected RenderingContextHandler _renderingContextHandler;
116    /** Source Resolver */
117    protected SourceResolver _srcResolver;
118    /** The notofication helper */
119    protected NotificationPreferencesHelper _notificationPrefHelper;
120    
121    @Override
122    public void initialize() throws Exception
123    {
124        _rightManager = (RightManager) _manager.lookup(RightManager.ROLE);
125        _userManager = (UserManager) _manager.lookup(UserManager.ROLE);
126        _workflowProvider = (WorkflowProvider) _manager.lookup(WorkflowProvider.ROLE);
127        _i18nUtils = (I18nUtils) _manager.lookup(I18nUtils.ROLE);
128        _resolver = (AmetysObjectResolver) _manager.lookup(AmetysObjectResolver.ROLE);
129        _projectManager = (ProjectManager) _manager.lookup(ProjectManager.ROLE);
130        _renderingContextHandler = (RenderingContextHandler) _manager.lookup(RenderingContextHandler.ROLE);
131        _srcResolver = (SourceResolver) _manager.lookup(SourceResolver.ROLE);
132        _notificationPrefHelper = (NotificationPreferencesHelper) _manager.lookup(NotificationPreferencesHelper.ROLE);
133    }
134    
135    @Override
136    public void setPluginInfo(String pluginName, String featureName, String id)
137    {
138        _pluginName = pluginName;
139    }
140    
141    public void contextualize(Context context) throws ContextException
142    {
143        _context = context;
144    }
145    
146    @Override
147    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
148    {
149        Boolean sendMail = (Boolean) transientVars.get("sendMail");
150        
151        if (BooleanUtils.isNotFalse(sendMail))
152        {
153            try
154            {
155                Request request = ContextHelper.getRequest(_context);
156                request.setAttribute("pluginName", _pluginName);
157                
158                String eventId = (String) transientVars.get("eventId");
159                CalendarEvent event = _resolver.resolveById(eventId);
160                
161                UserIdentity issuer = getUser(transientVars);
162                
163                String projectName = (String) request.getAttribute("projectName");
164                Project project = _projectManager.getProject(projectName);
165
166                String subjectI18nKey = StringUtils.defaultString((String) args.get(SUBJECT_KEY));
167                List<String> mailSubjectParams = getSubjectI18nParams(project, issuer, event);
168                Site site = project.getSite();
169                String lang = site.getSitemaps().iterator().next().getName();
170                I18nizableText i18nSubject = new I18nizableText(null, subjectI18nKey, mailSubjectParams);
171                String subject = _i18nUtils.translate(i18nSubject, lang);
172                
173                String mailBody;
174                Source source = null;
175                RenderingContext currentContext = _renderingContextHandler.getRenderingContext();
176
177                String titleKey = StringUtils.defaultString((String) args.get(BODY_KEY));
178                try
179                {
180                    // Force rendering context.FRONT to resolve URI
181                    _renderingContextHandler.setRenderingContext(RenderingContext.FRONT);
182                    request.setAttribute("forceAbsoluteUrl", true);
183                    request.setAttribute("lang", lang);
184                    request.setAttribute(WebConstants.REQUEST_ATTR_SITE, site);
185                    request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, site.getName());
186                    request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, site.getSkinId());
187                    source = _srcResolver.resolveURI("cocoon://_plugins/workspaces/notification-mail-calendar-event", null, Map.of("event", event, "project", project, "issuer", issuer, "titleKey", titleKey));
188                    try (InputStream is = source.getInputStream())
189                    {
190                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
191                        SourceUtil.copy(is, bos);
192                        mailBody = bos.toString("UTF-8");
193                    }
194                }
195                finally
196                {
197                    _renderingContextHandler.setRenderingContext(currentContext);
198                    if (source != null)
199                    {
200                        _srcResolver.release(source);
201                    }
202                }
203                String rights = StringUtils.defaultString((String) args.get(RIGHTS));
204
205                List<UserIdentity> recipients = getUsersToNotify(event.getId(), event.getParent(), rights);
206                recipients = recipients.stream().filter(userId -> _notificationPrefHelper.askedToBeNotified(userId, projectName, Frequency.EACH)).collect(Collectors.toList());
207                sendMail(recipients, subject, mailBody);
208            }
209            catch (Exception e)
210            {
211                _logger.error("An error occurred: unable to send mail to notify workflow change.", e);
212            }
213        }
214    }
215    
216    /**
217     * Sent an email
218     * @param recipients the users we want to send an email
219     * @param subject the subject of the mail
220     * @param htmlMailBody the mail body 
221     */
222    protected void sendMail(List<UserIdentity> recipients, String subject, String htmlMailBody)
223    {
224        List<String> recipientAdresses = recipients.stream()
225                .map(_userManager::getUser)
226                .filter(Objects::nonNull)
227                .map(User::getEmail)
228                .filter(StringUtils::isNotEmpty)
229                .collect(Collectors.toList());
230        
231        try
232        {
233            SendMailHelper.newMail()
234                          .withSubject(subject)
235                          .withHTMLBody(htmlMailBody)
236                          .withRecipients(recipientAdresses)
237                          .withAsync(true)
238                          .withInlineCSS(false)
239                          .sendMail();
240        }
241        catch (MessagingException | IOException e)
242        {
243            _logger.warn("Could not send an notification e-mail to " + recipients, e);
244        }
245    }
246    
247    /**
248     * Get the users allowed to be notified
249     * @param eventId The id of the event
250     * @param object The object responsible of the notification
251     * @param rightIds The id of rights to check
252     * @return The allowed users
253     */
254    protected List<UserIdentity> getUsersToNotify(String eventId, AmetysObject object, String rightIds)
255    {
256        boolean returnAll = Config.getInstance().getValue("runtime.mail.massive.sending");
257        Collection<UserIdentity> allowedUsers = _rightManager.getReadAccessAllowedUsers(object).resolveAllowedUsers(returnAll); 
258        for (String rightId : StringUtils.split(rightIds, ","))
259        {
260            allowedUsers = CollectionUtils.retainAll(allowedUsers, _rightManager.getAllowedUsers(rightId, object).resolveAllowedUsers(returnAll));
261        }
262        return new ArrayList<>(allowedUsers);
263    }
264    
265    /**
266     * Get the i18n parameters of mail subject
267     * @param project The the project
268     * @param issuer The issuer
269     * @param event The event
270     * @return The i18n parameters
271     */
272    protected List<String> getSubjectI18nParams (Project project, UserIdentity issuer, CalendarEvent event)
273    {
274        List<String> params = new ArrayList<>();
275        params.add(project.getTitle()); // {0}
276        
277        params.add(event.getTitle()); // {1}
278        
279        Calendar calendar = event.getParent();
280        params.add(calendar.getName()); // {2}
281        return params;
282    }
283}