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