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.util.ArrayList; 020import java.util.HashSet; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Set; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.activity.Initializable; 029import org.apache.avalon.framework.context.Context; 030import org.apache.avalon.framework.context.ContextException; 031import org.apache.avalon.framework.context.Contextualizable; 032import org.apache.cocoon.components.ContextHelper; 033import org.apache.cocoon.environment.Request; 034import org.apache.commons.lang.StringUtils; 035import org.apache.excalibur.source.SourceResolver; 036 037import org.ametys.cms.repository.WorkflowAwareContent; 038import org.ametys.core.right.RightManager; 039import org.ametys.core.user.User; 040import org.ametys.core.user.UserIdentity; 041import org.ametys.core.user.UserManager; 042import org.ametys.core.util.I18nUtils; 043import org.ametys.core.util.mail.SendMailHelper; 044import org.ametys.plugins.workflow.support.WorkflowProvider; 045import org.ametys.runtime.config.Config; 046import org.ametys.runtime.i18n.I18nizableText; 047import org.ametys.runtime.plugin.component.PluginAware; 048 049import com.opensymphony.module.propertyset.PropertySet; 050import com.opensymphony.workflow.FunctionProvider; 051import com.opensymphony.workflow.WorkflowException; 052 053import jakarta.mail.MessagingException; 054 055/** 056 * OS workflow function to send mail after an action is triggered. 057 */ 058public class SendMailFunction extends AbstractContentWorkflowComponent implements FunctionProvider, Initializable, PluginAware, Contextualizable 059{ 060 /** 061 * Provide "false" to prevent the function sending the mail. 062 * Useful when making large automatic workflow operations (for instance, when bulk importing and proposing in one action). 063 */ 064 public static final String SEND_MAIL = "send-mail"; 065 066 /** The rights key. */ 067 protected static final String RIGHTS_KEY = "rights"; 068 /** The mail subject key. */ 069 protected static final String SUBJECT_KEY = "subjectKey"; 070 /** The mail body key. */ 071 protected static final String BODY_KEY = "bodyKey"; 072 073 /** The rights manager. */ 074 protected RightManager _rightManager; 075 076 /** The users manager. */ 077 protected UserManager _userManager; 078 079 /** The source resolver. */ 080 protected SourceResolver _sourceResolver; 081 082 /** The workflow. */ 083 protected WorkflowProvider _workflowProvider; 084 085 /** The Avalon context. */ 086 protected Context _context; 087 088 /** The plugin name. */ 089 protected String _pluginName; 090 091 /** I18nUtils */ 092 protected I18nUtils _i18nUtils; 093 094 @Override 095 public void initialize() throws Exception 096 { 097 _rightManager = (RightManager) _manager.lookup(RightManager.ROLE); 098 _userManager = (UserManager) _manager.lookup(UserManager.ROLE); 099 _sourceResolver = (SourceResolver) _manager.lookup(SourceResolver.ROLE); 100 _workflowProvider = (WorkflowProvider) _manager.lookup(WorkflowProvider.ROLE); 101 _i18nUtils = (I18nUtils) _manager.lookup(I18nUtils.ROLE); 102 } 103 104 public void contextualize(Context context) throws ContextException 105 { 106 _context = context; 107 } 108 109 @Override 110 public void setPluginInfo(String pluginName, String featureName, String id) 111 { 112 _pluginName = pluginName; 113 } 114 115 @Override 116 public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException 117 { 118 String rightsParam = StringUtils.defaultString((String) args.get(RIGHTS_KEY)); 119 String subjectI18nKey = StringUtils.defaultString((String) args.get(SUBJECT_KEY)); 120 String bodyI18nKey = StringUtils.defaultString((String) args.get(BODY_KEY)); 121 122 Set<String> rights = new HashSet<>(); 123 for (String right : rightsParam.split(",")) 124 { 125 if (StringUtils.isNotBlank(right)) 126 { 127 rights.add(right.trim()); 128 } 129 } 130 131 // If "send-mail" is set to true or is not present in the vars, send the mail. 132 boolean dontSendMail = "false".equals(transientVars.get(SEND_MAIL)); 133 134 if (dontSendMail) 135 { 136 return; 137 } 138 139 try 140 { 141 WorkflowAwareContent content = getContent(transientVars); 142 143 Set<String> recipients = getRecipients(transientVars, content, rights); 144 145 if (recipients.size() > 0) 146 { 147 User caller = getCaller(transientVars, content); 148 String sender = getSender(transientVars, content); 149 String mailSubject = getMailSubject(subjectI18nKey, caller, content, transientVars); 150 String mailBody = getMailBody(bodyI18nKey, caller, content, transientVars); 151 152 _sendMails(mailSubject, mailBody, recipients, sender); 153 } 154 } 155 catch (Exception e) 156 { 157 _logger.error("An error occurred: unable to send mail to notify workflow change.", e); 158 } 159 } 160 161 /** 162 * Get the subject of mail 163 * @param subjectI18nKey the i18n key to use for subject 164 * @param user the caller 165 * @param content the content 166 * @param transientVars the transient variables 167 * @return the subject 168 */ 169 protected String getMailSubject (String subjectI18nKey, User user, WorkflowAwareContent content, Map transientVars) 170 { 171 I18nizableText subjectKey = new I18nizableText(null, subjectI18nKey, getSubjectI18nParams(user, content)); 172 return _i18nUtils.translate(subjectKey, null); // FIXME Use user preference language 173 } 174 175 /** 176 * Get the text body of mail 177 * @param bodyI18nKey the i18n key to use for body 178 * @param user the caller 179 * @param content the content 180 * @param transientVars the transient variables 181 * @return the text body 182 */ 183 protected String getMailBody (String bodyI18nKey, User user, WorkflowAwareContent content, Map transientVars) 184 { 185 I18nizableText bodyKey = new I18nizableText(null, bodyI18nKey, getBodyI18nParams(user, content)); 186 String mailBody = _i18nUtils.translate(bodyKey, null); // FIXME Use user preference language 187 188 // Get the workflow comment 189 String comment = (String) transientVars.get("comment"); 190 if (StringUtils.isNotEmpty(comment)) 191 { 192 List<String> params = new ArrayList<>(); 193 params.add(comment); 194 195 I18nizableText commentKey = new I18nizableText("plugin.cms", "WORKFLOW_MAIL_BODY_USER_COMMENT", params); 196 String commentTxt = _i18nUtils.translate(commentKey, null); // FIXME Use user preference language 197 198 mailBody += "\n\n" + commentTxt; 199 } 200 201 return mailBody; 202 } 203 204 /** 205 * Send the notification emails. 206 * @param subject the e-mail subject. 207 * @param body the e-mail body. 208 * @param recipients the recipients emails address. 209 * @param from the address sending the e-mail. 210 */ 211 protected void _sendMails(String subject, String body, Set<String> recipients, String from) 212 { 213 for (String recipient : recipients) 214 { 215 try 216 { 217 SendMailHelper.newMail() 218 .withSubject(subject) 219 .withTextBody(body) 220 .withSender(from) 221 .withRecipient(recipient) 222 .withAsync(true) 223 .sendMail(); 224 } 225 catch (MessagingException | IOException e) 226 { 227 _logger.warn("Could not send a workflow notification mail to " + recipient, e); 228 } 229 } 230 } 231 232 /** 233 * Get the i18n parameters of mail subject 234 * @param user the caller 235 * @param content the content 236 * @return the i18n parameters 237 */ 238 protected List<String> getSubjectI18nParams (User user, WorkflowAwareContent content) 239 { 240 List<String> params = new ArrayList<>(); 241 params.add(_contentHelper.getTitle(content)); 242 return params; 243 } 244 245 /** 246 * Get the i18n parameters of mail body text 247 * @param user the caller 248 * @param content the content 249 * @return the i18n parameters 250 */ 251 protected List<String> getBodyI18nParams (User user, WorkflowAwareContent content) 252 { 253 List<String> params = new ArrayList<>(); 254 255 params.add(user.getFullName()); // {0} 256 params.add(content.getTitle()); // {1} 257 params.add(_getContentUri(content)); // {2} 258 259 return params; 260 } 261 262 /** 263 * Get the content uri 264 * @param content the content 265 * @return the content uri 266 */ 267 protected String _getContentUri(WorkflowAwareContent content) 268 { 269 String requestUri = _getRequestUri(); 270 return requestUri + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27"; 271 } 272 273 /** 274 * Get the request URI. 275 * @return the full request URI. 276 */ 277 protected String _getRequestUri() 278 { 279 return StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 280 } 281 282 /** 283 * Retrieve the request from which this component is called. 284 * @return the request or <code>null</code>. 285 */ 286 public Request _getRequest() 287 { 288 try 289 { 290 return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT); 291 } 292 catch (ContextException ce) 293 { 294 _logger.info("Unable to get the request", ce); 295 return null; 296 } 297 } 298 299 /** 300 * Get the caller of the workflow action 301 * @param transientVars the transient variables 302 * @param content content the content 303 * @return caller the caller if the workflow function 304 * @throws WorkflowException if failed to get caller 305 */ 306 public User getCaller(Map transientVars, WorkflowAwareContent content) throws WorkflowException 307 { 308 UserIdentity userIdentity = getUser(transientVars); 309 return userIdentity != null ? _userManager.getUser(userIdentity) : null; 310 } 311 312 /** 313 * Get the sender for mail 314 * @param transientVars the transient variables 315 * @param content the content 316 * @return the sender email address 317 * @throws WorkflowException if failed to get email for sender 318 */ 319 protected String getSender(Map transientVars, WorkflowAwareContent content) throws WorkflowException 320 { 321 User user = getCaller(transientVars, content); 322 return user != null ? user.getEmail() : null; 323 } 324 325 /** 326 * Get the recipients 327 * @param transientVars the transient variables 328 * @param content the content. 329 * @param rights the set of rights to check. 330 * @return the recipients. 331 * @throws WorkflowException If failed to get recipients 332 */ 333 protected Set<String> getRecipients(Map transientVars, WorkflowAwareContent content, Set<String> rights) throws WorkflowException 334 { 335 Set<UserIdentity> users = _getUsers(content, rights); 336 337 return users.stream() 338 .map(_userManager::getUser) 339 .filter(Objects::nonNull) 340 .map(User::getEmail) 341 .filter(StringUtils::isNotBlank) 342 .collect(Collectors.toSet()); 343 } 344 345 /** 346 * Get the user logins. 347 * @param content the content. 348 * @param rights the set of rights to check. 349 * @return the users. 350 * @throws WorkflowException If an error occurred 351 */ 352 protected Set<UserIdentity> _getUsers(WorkflowAwareContent content, Set<String> rights) throws WorkflowException 353 { 354 Set<UserIdentity> users = new HashSet<>(); 355 356 Iterator<String> rightIt = rights.iterator(); 357 358 // First right : add all the granted users. 359 if (rightIt.hasNext()) 360 { 361 users.addAll(_rightManager.getAllowedUsers(rightIt.next(), content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"))); 362 } 363 364 // Next rights : retain all the granted users. 365 while (rightIt.hasNext()) 366 { 367 users.retainAll(_rightManager.getAllowedUsers(rightIt.next(), content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"))); 368 } 369 370 return users; 371 } 372}