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 return _contentHelper.getContentBOUrl(content, Map.of()); 290 } 291 292 /** 293 * Retrieve the request from which this component is called. 294 * @return the request or <code>null</code>. 295 */ 296 public Request _getRequest() 297 { 298 try 299 { 300 return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT); 301 } 302 catch (ContextException ce) 303 { 304 _logger.info("Unable to get the request", ce); 305 return null; 306 } 307 } 308 309 /** 310 * Get the caller of the workflow action 311 * @param transientVars the transient variables 312 * @param content content the content 313 * @return caller the caller if the workflow function 314 * @throws WorkflowException if failed to get caller 315 */ 316 public User getCaller(Map transientVars, WorkflowAwareContent content) throws WorkflowException 317 { 318 UserIdentity userIdentity = getUser(transientVars); 319 return userIdentity != null ? _userManager.getUser(userIdentity) : null; 320 } 321 322 /** 323 * Get the sender for mail 324 * @param transientVars the transient variables 325 * @param content the content 326 * @return the sender email address 327 * @throws WorkflowException if failed to get email for sender 328 */ 329 protected String getSender(Map transientVars, WorkflowAwareContent content) throws WorkflowException 330 { 331 User user = getCaller(transientVars, content); 332 return user != null ? user.getEmail() : null; 333 } 334 335 /** 336 * Get the recipients 337 * @param transientVars the transient variables 338 * @param content the content. 339 * @param rights the set of rights to check. 340 * @return the recipients. 341 * @throws WorkflowException If failed to get recipients 342 */ 343 protected Set<String> getRecipients(Map transientVars, WorkflowAwareContent content, Set<String> rights) throws WorkflowException 344 { 345 Set<UserIdentity> users = _getUsers(content, rights); 346 347 return users.stream() 348 .map(_userManager::getUser) 349 .filter(Objects::nonNull) 350 .map(User::getEmail) 351 .filter(StringUtils::isNotBlank) 352 .collect(Collectors.toSet()); 353 } 354 355 /** 356 * Get the user logins. 357 * @param content the content. 358 * @param rights the set of rights to check. 359 * @return the users. 360 * @throws WorkflowException If an error occurred 361 */ 362 protected Set<UserIdentity> _getUsers(WorkflowAwareContent content, Set<String> rights) throws WorkflowException 363 { 364 Set<UserIdentity> users = new HashSet<>(); 365 366 Iterator<String> rightIt = rights.iterator(); 367 368 // First right : add all the granted users. 369 if (rightIt.hasNext()) 370 { 371 users.addAll(_rightManager.getAllowedUsers(rightIt.next(), content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"))); 372 } 373 374 // Next rights : retain all the granted users. 375 while (rightIt.hasNext()) 376 { 377 users.retainAll(_rightManager.getAllowedUsers(rightIt.next(), content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"))); 378 } 379 380 return users; 381 } 382 383 @Override 384 public FunctionType getFunctionExecType() 385 { 386 return FunctionType.POST; 387 } 388 389 @Override 390 public I18nizableText getLabel() 391 { 392 return new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_FUNCTION_LABEL"); 393 } 394 395 @SuppressWarnings("unchecked") 396 @Override 397 public List<WorkflowArgument> getArguments() 398 { 399 WorkflowArgument rights = WorkflowElementDefinitionHelper.getElementDefinition( 400 RIGHTS_KEY, 401 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_RIGHTS_KEY_LABEL"), 402 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_RIGHTS_KEY_DESCRIPTION"), 403 false, 404 true 405 ); 406 StaticEnumerator<String> rightsStaticEnumerator = new StaticEnumerator<>(); 407 for (String rightId : _rightsExtensionPoint.getExtensionsIds()) 408 { 409 Right right = _rightsExtensionPoint.getExtension(rightId); 410 Map<String, I18nizableTextParameter> params = Map.of("category", right.getCategory(), "label", right.getLabel()); 411 rightsStaticEnumerator.add(new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_EDITOR_CHECK_RIGHTS_ARGUMENT_RIGHT_KEY_PARAMS_LABEL", params), right.getId()); 412 } 413 rights.setEnumerator(rightsStaticEnumerator); 414 415 return List.of( 416 WorkflowElementDefinitionHelper.getElementDefinition( 417 SUBJECT_KEY, 418 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_SUBJECT_KEY_LABEL"), 419 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_SUBJECT_KEY_DESCRIPTION"), 420 true, 421 false 422 ), 423 WorkflowElementDefinitionHelper.getElementDefinition( 424 BODY_KEY, 425 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_BODY_KEY_LABEL"), 426 new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_ARGUMENT_BODY_KEY_DESCRIPTION"), 427 true, 428 false 429 ), 430 rights 431 ); 432 } 433 434 @Override 435 public I18nizableText getFullLabel(Map<String, String> argumentsValues) 436 { 437 String rightsParam = StringUtils.defaultString(argumentsValues.get(RIGHTS_KEY)); 438 if (!rightsParam.isBlank()) 439 { 440 Object[] rightsIds = _getRights(rightsParam).toArray(); 441 Right right = _rightsExtensionPoint.getExtension((String) rightsIds[0]); 442 String concatenatedRights = "<strong>" + _i18nUtils.translate(right.getLabel()) + "</strong>"; 443 String and = _i18nUtils.translate(new I18nizableText("plugin.cms", "PLUGINS_CMS_CONDITION_AND")); 444 for (int i = 1; i < rightsIds.length; i++) 445 { 446 right = _rightsExtensionPoint.getExtension((String) rightsIds[i]); 447 concatenatedRights += and + "<strong>" + _i18nUtils.translate(right.getLabel()) + "</strong>"; 448 } 449 return new I18nizableText("plugin.cms", "PLUGINS_CMS_SEND_MAIL_FUNCTION_RIGHTS_DESCRIPTION", List.of(concatenatedRights)); 450 } 451 return getLabel(); 452 } 453}