001/* 002 * Copyright 2020 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.schedule; 017 018import java.io.IOException; 019import java.util.Optional; 020import java.util.Set; 021import java.util.stream.Collectors; 022 023import org.apache.avalon.framework.activity.Initializable; 024import org.apache.avalon.framework.service.ServiceException; 025import org.apache.avalon.framework.service.ServiceManager; 026import org.apache.commons.lang3.StringUtils; 027import org.quartz.JobExecutionContext; 028 029import org.ametys.core.DevMode; 030import org.ametys.core.DevMode.DEVMODE; 031import org.ametys.core.right.RightManager; 032import org.ametys.core.right.RightManager.RightResult; 033import org.ametys.core.schedule.progression.ContainerProgressionTracker; 034import org.ametys.core.user.CurrentUserProvider; 035import org.ametys.core.user.User; 036import org.ametys.core.user.UserIdentity; 037import org.ametys.core.user.directory.NotUniqueUserException; 038import org.ametys.core.user.population.UserPopulation; 039import org.ametys.core.user.population.UserPopulationDAO; 040import org.ametys.core.util.I18nUtils; 041import org.ametys.core.util.mail.SendMailHelper; 042import org.ametys.core.util.mail.SendMailHelper.MailBuilder; 043import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable; 044import org.ametys.plugins.core.ui.ObfuscatedException; 045import org.ametys.plugins.core.user.UserHelper; 046import org.ametys.runtime.config.Config; 047import org.ametys.runtime.i18n.I18nizableText; 048 049import jakarta.mail.MessagingException; 050 051/** 052 * Abstract schedulable that send an email at the end of the execution 053 * By default, the email is sent to the user that launched the schedulable. This behavior can be overridden thanks to the {@link #_getRecipient(JobExecutionContext)} method 054 * If this user has no email address and there is an error during the execution, an email is sent to the system administrator 055 */ 056public abstract class AbstractSendingMailSchedulable extends AbstractStaticSchedulable implements Initializable 057{ 058 /** The utils for i18n */ 059 protected I18nUtils _i18nUtils; 060 /** Mail sender */ 061 protected String _mailSender; 062 /** Sys admin mail */ 063 protected String _sysadminMail; 064 /** Current user provider */ 065 protected CurrentUserProvider _currentUserProvider; 066 /** User population DAO */ 067 protected UserPopulationDAO _populationDAO; 068 /** Right manager */ 069 protected RightManager _rightManager; 070 /** User helper */ 071 protected UserHelper _userHelper; 072 073 @Override 074 public void service(ServiceManager manager) throws ServiceException 075 { 076 super.service(manager); 077 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 078 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 079 _populationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 080 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 081 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 082 } 083 084 public void initialize() throws Exception 085 { 086 _mailSender = Config.getInstance().getValue("smtp.mail.from"); 087 _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto"); 088 } 089 090 @Override 091 public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 092 { 093 Optional<String> recipientMail = _getRecipient(context); 094 095 String defaultLanguage = _userLanguagesManager.getDefaultLanguage(); 096 String language = StringUtils.defaultIfBlank(_getRecipientLanguage(context), defaultLanguage); 097 098 I18nizableText mailSubject = null; 099 String mailBody = null; 100 try 101 { 102 _doExecute(context, progressionTracker); 103 104 mailSubject = _getSuccessMailSubject(context); 105 mailBody = _getSuccessMailBody(context, language); 106 } 107 catch (Exception e) 108 { 109 if (recipientMail.isEmpty()) 110 { 111 recipientMail = Optional.ofNullable(_sysadminMail) 112 .filter(StringUtils::isNotEmpty); 113 // No user language, use the default language 114 language = defaultLanguage; 115 } 116 117 if (recipientMail.isPresent()) 118 { 119 mailSubject = _getErrorMailSubject(context); 120 // obfuscate the exception before client rendering 121 Exception ex = _obfuscateException(e, recipientMail.get()); 122 mailBody = _getErrorMailBody(context, language, ex); 123 124 // rendering done. Reveal the exception for future logging 125 if (ex instanceof ObfuscatedException oEx) 126 { 127 oEx.reveal(); 128 } 129 130 throw ex; 131 } 132 else 133 { 134 throw e; 135 } 136 } 137 finally 138 { 139 if (recipientMail.isPresent() && StringUtils.isNotEmpty(mailBody)) 140 { 141 _sendMail(mailSubject, mailBody, recipientMail.get(), context, language); 142 } 143 } 144 } 145 146 private Exception _obfuscateException(Exception e, String recipient) 147 { 148 if (!DEVMODE.PRODUCTION.equals(DevMode.getDeveloperMode()) // Do not read from request to prevent override from request param 149 || _recipientIsAdmin(recipient)) 150 { 151 return e; 152 } 153 else 154 { 155 ObfuscatedException obfuscatedException = ObfuscatedException.obfuscate(e); 156 return obfuscatedException; 157 } 158 } 159 160 private boolean _recipientIsAdmin(String recipient) 161 { 162 if (recipient.equals(_sysadminMail)) 163 { 164 return true; 165 } 166 167 Set<String> populations = _populationDAO.getEnabledUserPopulations(true).stream() 168 .map(UserPopulation::getId) 169 .collect(Collectors.toUnmodifiableSet()); 170 try 171 { 172 User user = _userManager.getUserByEmail(populations, recipient); 173 return user != null && _rightManager.hasRight(user.getIdentity(), "Runtime_Rights_Admin_Access", "/admin") == RightResult.RIGHT_ALLOW; 174 } 175 catch (NotUniqueUserException e) 176 { 177 return false; 178 } 179 } 180 181 /** 182 * Executes the schedulable. 183 * @param context the context 184 * @param progressionTracker The progression tracker 185 * @throws Exception if an error occurred 186 */ 187 protected abstract void _doExecute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception; 188 189 /** 190 * Retrieves the language to use in the mail 191 * @param context the context 192 * @return the language of the recipient or null if none was found 193 */ 194 protected String _getRecipientLanguage(JobExecutionContext context) 195 { 196 UserIdentity userIdentity = _currentUserProvider.getUser(); 197 if (userIdentity != null) 198 { 199 User user = _userManager.getUser(userIdentity); 200 if (user != null) 201 { 202 return user.getLanguage(); 203 } 204 } 205 206 return null; 207 } 208 209 /** 210 * Retrieves the optional recipient of the mail 211 * @param context the context 212 * @return the optional recipient of the mail 213 */ 214 protected Optional<String> _getRecipient(JobExecutionContext context) 215 { 216 return Optional.of(_currentUserProvider) 217 .map(CurrentUserProvider::getUser) 218 .map(_userManager::getUser) 219 .map(User::getEmail) 220 .filter(StringUtils::isNotEmpty); 221 } 222 223 /** 224 * Determines if the mail body is in HTML 225 * @param context the context 226 * @return <code>true</code> if the mail body is in HTML, <code>false</code> otherwise 227 * @throws Exception If an error occurs while retrieving if mail body should be HTML 228 */ 229 protected boolean _isMailBodyInHTML(JobExecutionContext context) throws Exception 230 { 231 return false; 232 } 233 234 /** 235 * Retrieves the subject of the success mail 236 * @param context the context 237 * @return the subject of the success mail 238 * @throws Exception If an error occurs while building the mail subject 239 */ 240 protected abstract I18nizableText _getSuccessMailSubject(JobExecutionContext context) throws Exception; 241 242 /** 243 * Retrieves the body of the success mail 244 * @param context the context 245 * @param language The language to use. Should not be null. 246 * @return the body of the success mail 247 * @throws Exception If an error occurs while building the mail body 248 */ 249 protected abstract String _getSuccessMailBody(JobExecutionContext context, String language) throws Exception; 250 251 /** 252 * Retrieves the subject of the error mail 253 * @param context the context 254 * @return the subject of the error mail 255 * @throws Exception If an error occurs while building the mail subject 256 */ 257 protected abstract I18nizableText _getErrorMailSubject(JobExecutionContext context) throws Exception; 258 259 /** 260 * Retrieves the body of the error mail 261 * @param context the context 262 * @param throwable the error 263 * @param language The language to use. Should not be null. 264 * @return the body of the error mail 265 * @throws Exception If an error occurs while building the mail body 266 */ 267 protected abstract String _getErrorMailBody(JobExecutionContext context, String language, Throwable throwable) throws Exception; 268 269 /** 270 * Send an email 271 * @param subject the email's subject 272 * @param body the email's body (to HTML or text format depending on {@link #_isMailBodyInHTML(JobExecutionContext)} 273 * @param recipient the recipient address 274 * @param context the context 275 * @param language The language to use in the mail. Should not be null. 276 */ 277 protected void _sendMail (I18nizableText subject, String body, String recipient, JobExecutionContext context, String language) 278 { 279 try 280 { 281 MailBuilder mailBuilder = SendMailHelper.newMail() 282 .withSubject(_i18nUtils.translate(subject, language)) 283 .withRecipient(recipient) 284 .withSender(_mailSender); 285 286 if (_isMailBodyInHTML(context)) 287 { 288 mailBuilder.withHTMLBody(body); 289 } 290 else 291 { 292 mailBuilder.withTextBody(body); 293 } 294 295 mailBuilder.sendMail(); 296 } 297 catch (MessagingException | IOException e) 298 { 299 if (getLogger().isWarnEnabled()) 300 { 301 getLogger().warn("Unable to send the e-mail '" + subject + "' to '" + recipient + "'", e); 302 } 303 } 304 catch (Exception e) 305 { 306 getLogger().error("An unknown error has occured while sending the mail.", e); 307 } 308 } 309}