001/* 002 * Copyright 2023 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.web.usermanagement; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.cocoon.components.ContextHelper; 028import org.apache.cocoon.environment.Request; 029import org.apache.commons.lang3.StringUtils; 030import org.quartz.JobDataMap; 031import org.quartz.JobExecutionContext; 032 033import org.ametys.cms.schedule.AbstractSendingMailSchedulable; 034import org.ametys.core.schedule.progression.ContainerProgressionTracker; 035import org.ametys.core.ui.mail.StandardMailBodyHelper; 036import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder; 037import org.ametys.core.user.population.UserPopulation; 038import org.ametys.core.user.population.UserPopulationDAO; 039import org.ametys.plugins.core.schedule.Scheduler; 040import org.ametys.runtime.config.Config; 041import org.ametys.runtime.i18n.I18nizableText; 042import org.ametys.runtime.i18n.I18nizableTextParameter; 043import org.ametys.web.WebConstants; 044import org.ametys.web.repository.site.Site; 045import org.ametys.web.repository.site.SiteManager; 046import org.ametys.web.usermanagement.UserManagementException.StatusError; 047 048/** 049 * Job for sending invitations email 050 */ 051public class SendInvitationsSchedulable extends AbstractSendingMailSchedulable 052{ 053 /** The key for the id of the population */ 054 public static final String USER_POPULATION_ID_KEY = "populationId"; 055 056 /** The key for the id of the population */ 057 public static final String USER_DIRECTORY_ID_KEY = "userDirectoryId"; 058 059 /** The key for the site name */ 060 public static final String SITE_NAME_KEY = "siteName"; 061 062 /** The key for the guests */ 063 public static final String GUESTS_KEY = "guests"; 064 065 /** The key for the guests */ 066 public static final String RESEND_INVITATIONS_KEY = "resendInvitations"; 067 068 private static final String __JOBDATAMAP_USER_POPULATION_ID_KEY = Scheduler.PARAM_VALUES_PREFIX + USER_POPULATION_ID_KEY; 069 070 private static final String __JOBDATAMAP_USER_DIRECTORY_ID_KEY = Scheduler.PARAM_VALUES_PREFIX + USER_DIRECTORY_ID_KEY; 071 072 private static final String __JOBDATAMAP_SITE_NAME_KEY = Scheduler.PARAM_VALUES_PREFIX + SITE_NAME_KEY; 073 074 private static final String __JOBDATAMAP_GUESTS_KEY = Scheduler.PARAM_VALUES_PREFIX + GUESTS_KEY; 075 076 private static final String __JOBDATAMAP_RESEND_INVITATIONS_KEY = Scheduler.PARAM_VALUES_PREFIX + RESEND_INVITATIONS_KEY; 077 078 private static final String __GLOBAL_ERROR_CAUSE_KEY = SendInvitationsSchedulable.class.getName() + "$globalError"; 079 080 private static final String __INVALID_EMAILS_KEY = SendInvitationsSchedulable.class.getName() + "$invalidEmails"; 081 082 private static final String __ERROR_MAILS_KEY = SendInvitationsSchedulable.class.getName() + "$errorMails"; 083 084 private static final String __SUCCESS_EMAILS_KEY = SendInvitationsSchedulable.class.getName() + "$successEmails"; 085 086 private static final String __EXISTING_USERS_KEY = SendInvitationsSchedulable.class.getName() + "$existingUsers"; 087 088 private static final String __EXISTING_TEMP_USERS_KEY = SendInvitationsSchedulable.class.getName() + "$existingTempUsers"; 089 090 /** The signup manager */ 091 protected UserSignupManager _signupManager; 092 /** The site manager */ 093 protected SiteManager _siteManager; 094 /** DAO for user population */ 095 protected UserPopulationDAO _userPopulationDAO; 096 /** The CMS base url */ 097 private String _cmsUrl; 098 099 @Override 100 public void service(ServiceManager smanager) throws ServiceException 101 { 102 super.service(smanager); 103 _signupManager = (UserSignupManager) smanager.lookup(UserSignupManager.ROLE); 104 _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); 105 _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE); 106 } 107 108 @Override 109 public void initialize() throws Exception 110 { 111 super.initialize(); 112 _cmsUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 113 } 114 115 @Override 116 protected void _doExecute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 117 { 118 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 119 120 String populationId = jobDataMap.getString(__JOBDATAMAP_USER_POPULATION_ID_KEY); 121 String userDirectoryId = jobDataMap.getString(__JOBDATAMAP_USER_DIRECTORY_ID_KEY); 122 String siteName = jobDataMap.getString(__JOBDATAMAP_SITE_NAME_KEY); 123 boolean resendInvitations = jobDataMap.getBooleanValue(__JOBDATAMAP_RESEND_INVITATIONS_KEY); 124 125 @SuppressWarnings("unchecked") 126 List<String> guestUserLines = (List<String>) jobDataMap.get(__JOBDATAMAP_GUESTS_KEY); 127 128 List<String> successEmails = new ArrayList<>(); 129 List<String> invalidEmails = new ArrayList<>(); 130 List<String> errorMails = new ArrayList<>(); 131 List<String> existingUsers = new ArrayList<>(); 132 List<String> existingTempUsers = new ArrayList<>(); 133 134 Request request = ContextHelper.getRequest(_context); 135 136 // Set the site name to be able to check rights 137 request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, siteName); 138 139 for (String line : guestUserLines) 140 { 141 String[] columns = Arrays.stream(line.split(";")).map(StringUtils::normalizeSpace).toArray(String[]::new); 142 143 String email = columns[0]; 144 String lastname = columns.length > 1 ? columns[1] : null; 145 String firstname = columns.length > 2 ? columns[2] : null; 146 147 try 148 { 149 _signupManager.inviteToSignup(siteName, null, email, populationId, userDirectoryId, lastname, firstname, true, resendInvitations, true); 150 successEmails.add(email); 151 } 152 catch (UserManagementException e) 153 { 154 StatusError errorCause = e.getStatusError(); 155 switch (errorCause) 156 { 157 case NO_SIGNUP_PAGE: 158 case SIGNUP_NOT_ALLOWED: 159 case USER_NOT_ALLOWED: 160 context.put(__GLOBAL_ERROR_CAUSE_KEY, errorCause); 161 throw new IllegalArgumentException("Invalid configuration to send invitation", e); 162 case USER_ALREADY_EXISTS: 163 existingUsers.add(email); 164 break; 165 case INVALID_EMAIL: 166 invalidEmails.add(email); 167 break; 168 case MAIL_ERROR: 169 errorMails.add(email); 170 break; 171 case TEMP_USER_ALREADY_EXISTS: 172 existingTempUsers.add(email); 173 break; 174 default: 175 throw new IllegalArgumentException("Unexpected error: " + errorCause, e); 176 } 177 } 178 } 179 180 context.put(__SUCCESS_EMAILS_KEY, successEmails); 181 context.put(__ERROR_MAILS_KEY, errorMails); 182 context.put(__INVALID_EMAILS_KEY, invalidEmails); 183 context.put(__EXISTING_TEMP_USERS_KEY, existingTempUsers); 184 context.put(__EXISTING_USERS_KEY, existingUsers); 185 } 186 187 @Override 188 protected I18nizableText _getSuccessMailSubject(JobExecutionContext context) throws Exception 189 { 190 Site site = _getSite(context); 191 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 192 193 return new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_SUBJECT", List.of(siteTitle)); 194 } 195 196 @SuppressWarnings("unchecked") 197 @Override 198 protected String _getSuccessMailBody(JobExecutionContext context) throws Exception 199 { 200 List<String> successEmails = (List<String>) context.get(__SUCCESS_EMAILS_KEY); 201 List<String> invalidEmails = (List<String>) context.get(__INVALID_EMAILS_KEY); 202 List<String> errorMails = (List<String>) context.get(__ERROR_MAILS_KEY); 203 List<String> existingTempUsers = (List<String>) context.get(__EXISTING_TEMP_USERS_KEY); 204 List<String> existingUsers = (List<String>) context.get(__EXISTING_USERS_KEY); 205 206 return _buildMailBody(context, successEmails, invalidEmails, errorMails, existingTempUsers, existingUsers); 207 } 208 209 @Override 210 protected boolean _isMailBodyInHTML(JobExecutionContext context) throws Exception 211 { 212 return true; 213 } 214 215 /** 216 * Build the HTML mail body 217 * 218 * @param context the job context data 219 * @param successEmails the mails in success 220 * @param invalidEmails the invalid emails 221 * @param errorMails the mails for which the send has failed 222 * @param existingTempUsers the existing temporary users 223 * @param existingUsers the existing users 224 * @return the HTML mail body 225 * @throws IOException if failed to build send invitation report 226 */ 227 protected String _buildMailBody(JobExecutionContext context, List<String> successEmails, List<String> invalidEmails, List<String> errorMails, List<String> existingTempUsers, List<String> existingUsers) throws IOException 228 { 229 Site site = _getSite(context); 230 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 231 232 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 233 234 i18nParams.put("siteTitle", new I18nizableText(siteTitle)); 235 i18nParams.put("siteUrl", new I18nizableText(site.getUrl())); 236 i18nParams.put("count", new I18nizableText(String.valueOf(successEmails.size()))); 237 238 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 239 .withTitle(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_TITLE")); 240 241 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY", i18nParams)); 242 243 if (!invalidEmails.isEmpty()) 244 { 245 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 246 errorI18nParams.put("count", new I18nizableText(String.valueOf(invalidEmails.size()))); 247 248 List<String> formatedEmails = invalidEmails.stream() 249 .map(email -> "<strong>" + email + "</strong>") 250 .toList(); 251 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 252 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_INVALID_EMAILS_ERROR", errorI18nParams)); 253 } 254 255 if (!errorMails.isEmpty()) 256 { 257 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 258 errorI18nParams.put("count", new I18nizableText(String.valueOf(errorMails.size()))); 259 List<String> formatedEmails = errorMails.stream() 260 .map(email -> "<strong>" + email + "</strong>") 261 .toList(); 262 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 263 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_MAILS_ERROR", errorI18nParams)); 264 } 265 266 if (!existingTempUsers.isEmpty()) 267 { 268 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 269 errorI18nParams.put("count", new I18nizableText(String.valueOf(existingTempUsers.size()))); 270 List<String> formatedEmails = existingTempUsers.stream() 271 .map(email -> "<strong>" + email + "</strong>") 272 .toList(); 273 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 274 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_TEMP_USERS_EXIST_ERROR", errorI18nParams)); 275 } 276 277 if (!existingUsers.isEmpty()) 278 { 279 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 280 errorI18nParams.put("count", new I18nizableText(String.valueOf(existingUsers.size()))); 281 List<String> formatedEmails = existingUsers.stream() 282 .map(email -> "<strong>" + email + "</strong>") 283 .toList(); 284 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 285 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_USERS_EXIST_ERROR", errorI18nParams)); 286 } 287 288 // TODO check user rights 289 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_END", Map.of("cmsToolUri", new I18nizableText(getToolUri(site))))); 290 291 return bodyBuilder.build(); 292 } 293 294 /** 295 * Get the back-office url to access user temp tool 296 * @param site the site 297 * @return the tool uri 298 */ 299 protected String getToolUri(Site site) 300 { 301 StringBuilder url = new StringBuilder(_cmsUrl); 302 303 url.append("/" + site.getName()); 304 url.append("/index.html?uitool=uitool-temp-users"); 305 306 return url.toString(); 307 } 308 309 @Override 310 protected I18nizableText _getErrorMailSubject(JobExecutionContext context) throws Exception 311 { 312 Site site = _getSite(context); 313 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 314 return new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_SUBJECT", List.of(siteTitle)); 315 } 316 317 @Override 318 protected String _getErrorMailBody(JobExecutionContext context, Throwable throwable) throws Exception 319 { 320 StatusError errorCause = (StatusError) context.get(__GLOBAL_ERROR_CAUSE_KEY); 321 322 return _buildErrorMailBody(context, errorCause); 323 } 324 325 /** 326 * Build the HTML mail body in case of error 327 * 328 * @param context the job context data 329 * @param globalErrorCause the global error cause 330 * @return the HTML mail body in case of error 331 * @throws IOException if failed to build send invitation error 332 */ 333 protected String _buildErrorMailBody(JobExecutionContext context, StatusError globalErrorCause) throws IOException 334 { 335 Site site = _getSite(context); 336 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 337 338 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 339 340 i18nParams.put("siteTitle", new I18nizableText(siteTitle)); 341 i18nParams.put("siteUrl", new I18nizableText(site.getUrl())); 342 343 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 344 .withTitle(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY_TITLE")); 345 346 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY", i18nParams)); 347 348 if (globalErrorCause != null) 349 { 350 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 351 352 UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(jobDataMap.getString(__JOBDATAMAP_USER_POPULATION_ID_KEY)); 353 354 switch (globalErrorCause) 355 { 356 case NO_SIGNUP_PAGE: 357 i18nParams.put("population", userPopulation.getLabel()); 358 bodyBuilder.withDetails(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_ERROR_TITLE"), new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_NO_SIGNUP_PAGE_ERROR", i18nParams), false); 359 break; 360 case SIGNUP_NOT_ALLOWED: 361 bodyBuilder.withDetails(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_ERROR_TITLE"), new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_NO_SIGNUP_ALLOWED_ERROR"), false); 362 break; 363 case USER_NOT_ALLOWED: 364 i18nParams.put("population", userPopulation.getLabel()); 365 bodyBuilder.withDetails(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_ERROR_TITLE"), new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_USER_NOT_ALLOWED_ERROR", i18nParams), false); 366 break; 367 default: 368 bodyBuilder.withDetails(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_ERROR_TITLE"), new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY_SEE_LOGS"), false); 369 break; 370 } 371 } 372 else 373 { 374 bodyBuilder.withDetails(null, new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY_SEE_LOGS"), false); 375 } 376 377 return bodyBuilder.build(); 378 } 379 380 /** 381 * Get the site title from job execution context 382 * 383 * @param context the job context 384 * @return the site title 385 */ 386 protected Site _getSite(JobExecutionContext context) 387 { 388 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 389 String siteName = (String) jobDataMap.get(__JOBDATAMAP_SITE_NAME_KEY); 390 391 return _siteManager.getSite(siteName); 392 } 393 394}