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}