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}