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.getSites().iterator().next(); 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}