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