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 I18nizableText mailSubjectKey = getMailSubject(subjectI18nKey, caller, content, transientVars); 164 MailBodyBuilder mailBodyBuilder = getMailBody(subjectI18nKey, bodyI18nKey, caller, content, transientVars); 165 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 * @return the subject 195 */ 196 protected I18nizableText getMailSubject (String subjectI18nKey, User user, WorkflowAwareContent content, Map transientVars) 197 { 198 return new I18nizableText(null, subjectI18nKey, getSubjectI18nParams(user, content)); 199 } 200 201 /** 202 * Get the text body of mail 203 * @param subjectI18nKey the i18n key to use for body's title 204 * @param bodyI18nKey the i18n key to use for body 205 * @param user the caller 206 * @param content the content 207 * @param transientVars the transient variables 208 * @return the text body 209 * @throws IOException if an error occurred while building HTML workflow email 210 */ 211 protected MailBodyBuilder getMailBody (String subjectI18nKey, String bodyI18nKey, User user, WorkflowAwareContent content, Map transientVars) throws IOException 212 { 213 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 214 .withMessage(new I18nizableText(null, bodyI18nKey, getBodyI18nParams(user, content))) 215 .withLink(_getContentUri(content), new I18nizableText("plugin.cms", "WORKFLOW_MAIL_BODY_GO_TO_CONTENT")); 216 217 // Get the workflow comment 218 String comment = (String) transientVars.get("comment"); 219 if (StringUtils.isNotEmpty(comment)) 220 { 221 bodyBuilder.withUserInputs(List.of(new UserInput(user, ZonedDateTime.now(), comment)), new I18nizableText("plugin.cms", "WORKFLOW_MAIL_BODY_USER_COMMENT")); 222 } 223 224 return bodyBuilder; 225 } 226 227 /** 228 * Send the notification emails. 229 * @param subjectI18nKey the e-mail subject. 230 * @param bodyBuilder the e-mail body builder. 231 * @param recipientsByLanguage the recipients emails address by language. 232 * @param from the address sending the e-mail. 233 * @throws IOException If an error occurs while building the body 234 */ 235 protected void _sendMails(I18nizableText subjectI18nKey, MailBodyBuilder bodyBuilder, Map<String, Set<String>> recipientsByLanguage, String from) throws IOException 236 { 237 for (String language : recipientsByLanguage.keySet()) 238 { 239 String subject = _i18nUtils.translate(subjectI18nKey, language); 240 241 String body = bodyBuilder.withTitle(subject) 242 .withLanguage(language) 243 .build(); 244 245 for (String recipient : recipientsByLanguage.get(language)) 246 { 247 try 248 { 249 SendMailHelper.newMail() 250 .withSubject(subject) 251 .withHTMLBody(body) 252 .withSender(from) 253 .withRecipient(recipient) 254 .withAsync(true) 255 .sendMail(); 256 } 257 catch (MessagingException | IOException e) 258 { 259 _logger.warn("Could not send a workflow notification mail to " + recipient, e); 260 } 261 } 262 } 263 } 264 265 /** 266 * Get the i18n parameters of mail subject 267 * @param user the caller 268 * @param content the content 269 * @return the i18n parameters 270 */ 271 protected List<String> getSubjectI18nParams (User user, WorkflowAwareContent content) 272 { 273 List<String> params = new ArrayList<>(); 274 params.add(_contentHelper.getTitle(content)); 275 return params; 276 } 277 278 /** 279 * Get the i18n parameters of mail body text 280 * @param user the caller 281 * @param content the content 282 * @return the i18n parameters 283 */ 284 protected List<String> getBodyI18nParams (User user, WorkflowAwareContent content) 285 { 286 List<String> params = new ArrayList<>(); 287 288 params.add(user.getFullName()); // {0} 289 params.add(content.getTitle()); // {1} 290 params.add(_getContentUri(content)); // {2} 291 292 return params; 293 } 294 295 /** 296 * Get the content uri 297 * @param content the content 298 * @return the content uri 299 */ 300 protected String _getContentUri(WorkflowAwareContent content) 301 { 302 return _contentHelper.getContentBOUrl(content, Map.of()); 303 } 304 305 /** 306 * Retrieve the request from which this component is called. 307 * @return the request or <code>null</code>. 308 */ 309 public Request _getRequest() 310 { 311 try 312 { 313 return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT); 314 } 315 catch (ContextException ce) 316 { 317 _logger.info("Unable to get the request", ce); 318 return null; 319 } 320 } 321 322 /** 323 * Get the caller of the workflow action 324 * @param transientVars the transient variables 325 * @param content content the content 326 * @return caller the caller if the workflow function 327 * @throws WorkflowException if failed to get caller 328 */ 329 public User getCaller(Map transientVars, WorkflowAwareContent content) throws WorkflowException 330 { 331 UserIdentity userIdentity = getUser(transientVars); 332 return userIdentity != null ? _userManager.getUser(userIdentity) : null; 333 } 334 335 /** 336 * Get the sender for mail 337 * @param transientVars the transient variables 338 * @param content the content 339 * @return the sender email address 340 * @throws WorkflowException if failed to get email for sender 341 */ 342 protected String getSender(Map transientVars, WorkflowAwareContent content) throws WorkflowException 343 { 344 User user = getCaller(transientVars, content); 345 return user != null ? user.getEmail() : null; 346 } 347 348 /** 349 * Get the recipients 350 * @param transientVars the transient variables 351 * @param content the content. 352 * @param rights the set of rights to check. 353 * @return the recipients. 354 * @throws WorkflowException If failed to get recipients 355 */ 356 protected Map<String, Set<String>> getRecipientsByLanguage(Map transientVars, WorkflowAwareContent content, Set<String> rights) throws WorkflowException 357 { 358 Set<UserIdentity> users = _getUsers(content, rights); 359 360 String defaultLanguage = _userLanguagesManager.getDefaultLanguage(); 361 362 return users.stream() 363 .map(_userManager::getUser) 364 .filter(Objects::nonNull) 365 .map(user -> Pair.of(user.getLanguage(), user.getEmail())) 366 .filter(p -> StringUtils.isNotEmpty(p.getRight())) 367 .collect(Collectors.groupingBy( 368 p -> { 369 return StringUtils.defaultIfBlank(p.getLeft(), defaultLanguage); 370 }, 371 Collectors.mapping( 372 Pair::getRight, 373 Collectors.toSet() 374 ) 375 ) 376 ); 377 } 378 379 /** 380 * Get the user logins. 381 * @param content the content. 382 * @param rights the set of rights to check. 383 * @return the users. 384 * @throws WorkflowException If an error occurred 385 */ 386 protected Set<UserIdentity> _getUsers(WorkflowAwareContent content, Set<String> rights) throws WorkflowException 387 { 388 Set<UserIdentity> users = new HashSet<>(); 389 390 Iterator<String> rightIt = rights.iterator(); 391 392 // First right : add all the granted users. 393 if (rightIt.hasNext()) 394 { 395 users.addAll(_rightManager.getAllowedUsers(rightIt.next(), content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"))); 396 } 397 398 // Next rights : retain all the granted users. 399 while (rightIt.hasNext()) 400 { 401 users.retainAll(_rightManager.getAllowedUsers(rightIt.next(), content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"))); 402 } 403 404 return users; 405 } 406 407 @Override 408 public FunctionType getFunctionExecType() 409 { 410 return FunctionType.POST; 411 } 412 413 @Override 414 public I18nizableText getLabel() 415 { 416 return new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_FUNCTION_LABEL"); 417 } 418 419 @SuppressWarnings("unchecked") 420 @Override 421 public List<WorkflowArgument> getArguments() 422 { 423 WorkflowArgument rights = WorkflowElementDefinitionHelper.getElementDefinition( 424 RIGHTS_KEY, 425 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_RIGHTS_KEY_LABEL"), 426 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_RIGHTS_KEY_DESCRIPTION"), 427 false, 428 true 429 ); 430 StaticEnumerator<String> rightsStaticEnumerator = new StaticEnumerator<>(); 431 for (String rightId : _rightsExtensionPoint.getExtensionsIds()) 432 { 433 Right right = _rightsExtensionPoint.getExtension(rightId); 434 Map<String, I18nizableTextParameter> params = Map.of("category", right.getCategory(), "label", right.getLabel()); 435 rightsStaticEnumerator.add(new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_EDITOR_CHECK_RIGHTS_ARGUMENT_RIGHT_KEY_PARAMS_LABEL", params), right.getId()); 436 } 437 rights.setEnumerator(rightsStaticEnumerator); 438 439 return List.of( 440 WorkflowElementDefinitionHelper.getElementDefinition( 441 SUBJECT_KEY, 442 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_SUBJECT_KEY_LABEL"), 443 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_SUBJECT_KEY_DESCRIPTION"), 444 true, 445 false 446 ), 447 WorkflowElementDefinitionHelper.getElementDefinition( 448 BODY_KEY, 449 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_BODY_KEY_LABEL"), 450 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_BODY_KEY_DESCRIPTION"), 451 true, 452 false 453 ), 454 rights 455 ); 456 } 457 458 @Override 459 public I18nizableText getFullLabel(Map<String, String> argumentsValues) 460 { 461 String rightsParam = StringUtils.defaultString(argumentsValues.get(RIGHTS_KEY)); 462 if (!rightsParam.isBlank()) 463 { 464 Object[] rightsIds = _getRights(rightsParam).toArray(); 465 Right right = _rightsExtensionPoint.getExtension((String) rightsIds[0]); 466 String concatenatedRights = "<strong>" + _i18nUtils.translate(right.getLabel()) + "</strong>"; 467 String and = _i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_CONDITION_AND")); 468 for (int i = 1; i < rightsIds.length; i++) 469 { 470 right = _rightsExtensionPoint.getExtension((String) rightsIds[i]); 471 concatenatedRights += and + "<strong>" + _i18nUtils.translate(right.getLabel()) + "</strong>"; 472 } 473 return new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_FUNCTION_RIGHTS_DESCRIPTION", List.of(concatenatedRights)); 474 } 475 return getLabel(); 476 } 477}