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