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