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.alerts;
017
018import java.time.LocalDate;
019import java.time.ZonedDateTime;
020import java.util.ArrayList;
021import java.util.Calendar;
022import java.util.Collections;
023import java.util.Date;
024import java.util.GregorianCalendar;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029
030import javax.mail.MessagingException;
031
032import org.apache.avalon.framework.configuration.Configuration;
033import org.apache.avalon.framework.configuration.ConfigurationException;
034import org.apache.avalon.framework.context.Context;
035import org.apache.avalon.framework.context.ContextException;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.cocoon.Constants;
039import org.apache.cocoon.components.ContextHelper;
040import org.apache.cocoon.environment.Request;
041import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
042import org.apache.commons.lang.StringUtils;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046import org.ametys.cms.content.archive.ArchiveConstants;
047import org.ametys.cms.repository.Content;
048import org.ametys.cms.repository.ContentQueryHelper;
049import org.ametys.cms.repository.ModifiableContent;
050import org.ametys.cms.repository.WorkflowStepExpression;
051import org.ametys.cms.repository.WorkflowStepExpression.LogicalOperator;
052import org.ametys.core.engine.BackgroundEngineHelper;
053import org.ametys.core.right.RightManager;
054import org.ametys.core.user.User;
055import org.ametys.core.user.UserIdentity;
056import org.ametys.core.user.UserManager;
057import org.ametys.core.user.population.PopulationContextHelper;
058import org.ametys.core.util.DateUtils;
059import org.ametys.core.util.I18nUtils;
060import org.ametys.core.util.mail.SendMailHelper;
061import org.ametys.plugins.repository.AmetysObjectIterable;
062import org.ametys.plugins.repository.AmetysObjectResolver;
063import org.ametys.plugins.repository.AmetysRepositoryException;
064import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
065import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
066import org.ametys.plugins.repository.query.expression.AndExpression;
067import org.ametys.plugins.repository.query.expression.BooleanExpression;
068import org.ametys.plugins.repository.query.expression.DateExpression;
069import org.ametys.plugins.repository.query.expression.Expression;
070import org.ametys.plugins.repository.query.expression.Expression.Operator;
071import org.ametys.plugins.repository.query.expression.MetadataExpression;
072import org.ametys.plugins.repository.query.expression.NotExpression;
073import org.ametys.plugins.repository.query.expression.OrExpression;
074import org.ametys.plugins.repository.version.DataAndVersionAwareAmetysObject;
075import org.ametys.plugins.repository.version.ModifiableDataAwareVersionableAmetysObject;
076import org.ametys.runtime.config.Config;
077import org.ametys.runtime.i18n.I18nizableText;
078
079/**
080 * Alerts engine: sends alerts mail.
081 */
082public class AlertEngine implements Runnable
083{
084    
085    /** The logger. */
086    protected static final Logger _LOGGER = LoggerFactory.getLogger(AlertEngine.class);
087    
088    /** The avalon context. */
089    protected Context _context;
090    
091    /** The service manager. */
092    protected ServiceManager _manager;
093    
094    /** The server base URL. */
095    protected String _baseUrl;
096    
097    /** Is the engine initialized ? */
098    protected boolean _initialized;
099    
100    /** The cocoon environment context. */
101    protected org.apache.cocoon.environment.Context _environmentContext;
102    
103    /** The ametys object resolver. */
104    protected AmetysObjectResolver _ametysResolver;
105    
106    /** The rights manager. */
107    protected RightManager _rightManager;
108    
109    /** The users manager. */
110    protected UserManager _userManager;
111    
112    /** The i18n utils. */
113    protected I18nUtils _i18nUtils;
114    
115    /** The content of "from" field in emails. */
116    protected String _mailFrom;
117    
118    /** The "waiting for validation" e-mail will be sent to users that have at least one of this rights. */
119    protected Set<String> _awaitingValidationRights;
120    
121    /** The "waiting for validation" e-mail subject i18n key. */
122    protected String _awaitingValidationSubject;
123    
124    /** The "waiting for validation" e-mail body i18n key. */
125    protected String _awaitingValidationBody;
126    
127    /** Only contents in this steps will be taken into account for the "unmodified content" alert. */
128    protected int[] _unmodifiedContentStepIds;
129    
130    /** The "unmodified content" e-mail will be sent to users that have at least one of this rights. */
131    protected Set<String> _unmodifiedContentRights;
132    
133    /** The "unmodified content" e-mail subject i18n key. */
134    protected String _unmodifiedContentSubject;
135    
136    /** The "unmodified content" e-mail body i18n key. */
137    protected String _unmodifiedContentBody;
138    
139    /** The reminder e-mail will be sent to users that have this at least one of this rights. */
140    protected Set<String> _reminderRights;
141    
142    /** The reminder e-mail subject i18n key. */
143    protected String _reminderSubject;
144    
145    /** The reminder e-mail body i18n key. */
146    protected String _reminderBody;
147    
148    /** The scheduled archiving reminder e-mail will be sent to users that have this at least one of this rights. */
149    protected Set<String> _scheduledArchivingReminderRights;
150    
151    /** The scheduled archiving reminder e-mail subject i18n key. */
152    protected String _scheduledArchivingReminderSubject;
153    
154    /** The scheduled archiving reminder e-mail body i18n key. */
155    protected String _scheduledArchivingReminderBody;
156   
157    /** The instant alert e-mail will be sent to users that have this at least one of this rights. */
158    protected Set<String> _instantAlertRights;
159    /** The instant alert e-mail subject i18n key. */
160    protected String _instantAlertSubject;
161    /** The instant alert e-mail body i18n key. */
162    protected String _instantAlertBody;
163    
164    /** True if the engine was been run in instant mode (for instant alerts only) **/
165    protected boolean _instantMode;
166    /** The list of contents' id (for instant alerts only) **/
167    private List<String> _instantAlertContentIds;
168    /** The email message (for instant alerts only) **/
169    private String _instantAlertMessage;
170    
171    /**
172     * Default constructor
173     */
174    public AlertEngine ()
175    {
176        _instantMode = false;
177    }
178    
179    /**
180     * Constructor used to send instant alerts
181     * @param contentIds The content's id
182     * @param message the message
183     */
184    public AlertEngine (List<String> contentIds, String message)
185    {
186        _instantMode = true;
187        _instantAlertContentIds = contentIds;
188        _instantAlertMessage = message;
189    }
190    
191    /**
192     * Initialize the alert engine.
193     * @param manager the avalon service manager.
194     * @param context the avalon context.
195     * @throws ContextException if the CONTEXT_ENVIRONMENT_CONTEXT cannot be found
196     * @throws ServiceException if some components cannot be resolved
197     */
198    public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException
199    {
200        _manager = manager;
201        _context = context;
202        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
203        
204        // Lookup the needed components.
205        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
206        
207        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
208        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
209        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
210        
211        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
212        _mailFrom = Config.getInstance().getValue("smtp.mail.from");
213        
214        _initialized = true;
215    }
216    
217    /**
218     * Configure the engine (called by the scheduler).
219     * @param configuration the component configuration.
220     * @throws ConfigurationException if the configuration is not valid
221     */
222    public void configure(Configuration configuration) throws ConfigurationException
223    {
224        Configuration instantConf = configuration.getChild("instantAlert");
225        Configuration validationConf = configuration.getChild("awaitingValidation");
226        Configuration unmodifiedConf = configuration.getChild("unmodifiedContent");
227        Configuration reminderConf = configuration.getChild("reminder");
228        Configuration scheduledArchivingReminderConf = configuration.getChild("scheduledArchiving");
229        
230        // Configure the rights.
231        _instantAlertRights = _getRightsFromConf(instantConf);
232        _awaitingValidationRights = _getRightsFromConf(validationConf);
233        _unmodifiedContentRights = _getRightsFromConf(unmodifiedConf);
234        _reminderRights = _getRightsFromConf(reminderConf);
235        _scheduledArchivingReminderRights = _getRightsFromConf(scheduledArchivingReminderConf);
236        Configuration[] stepIds = unmodifiedConf.getChildren("stepId");
237        _unmodifiedContentStepIds = new int[stepIds.length];
238        int i = 0;
239        for (Configuration stepId : stepIds)
240        {
241            try
242            {
243                _unmodifiedContentStepIds[i] = Integer.parseInt(stepId.getValue(""));
244                i++;
245            }
246            catch (NumberFormatException e)
247            {
248                // Ignore
249            }
250        }
251        
252        // Configure the i18n texts.
253        _awaitingValidationSubject = validationConf.getChild("subjectKey").getValue();
254        _awaitingValidationBody = validationConf.getChild("bodyKey").getValue();
255        _unmodifiedContentSubject = unmodifiedConf.getChild("subjectKey").getValue();
256        _unmodifiedContentBody = unmodifiedConf.getChild("bodyKey").getValue();
257        _reminderSubject = reminderConf.getChild("subjectKey").getValue();
258        _reminderBody = reminderConf.getChild("bodyKey").getValue();
259        _scheduledArchivingReminderSubject = scheduledArchivingReminderConf.getChild("subjectKey").getValue();
260        _scheduledArchivingReminderBody = scheduledArchivingReminderConf.getChild("bodyKey").getValue();
261        _instantAlertSubject = instantConf.getChild("subjectKey").getValue();
262        _instantAlertBody = instantConf.getChild("bodyKey").getValue();
263    }
264    
265    /**
266     * Check the initialization and throw an exception if not initialized.
267     */
268    protected void _checkInitialization()
269    {
270        if (!_initialized)
271        {
272            String message = "Le composant de synchronisation doit être initialisé avant d'être lancé.";
273            _LOGGER.error(message);
274            throw new IllegalStateException(message);
275        }
276    }
277    
278    @Override
279    public void run()
280    {
281        Map<String, Object> environmentInformation = null;
282        try
283        {
284            _LOGGER.info("Preparing to send the alerts...");
285            
286            _checkInitialization();
287            
288            // Create the environment.
289            environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_LOGGER));
290            
291            // Prepare and send all the alerts.
292            _sendAlerts();
293        }
294        catch (Exception e)
295        {
296            _LOGGER.error("An error occurred sending the alerts.", e);
297        }
298        finally
299        {
300            // Leave the environment.
301            if (environmentInformation != null)
302            {
303                BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation);
304            }
305            // Dispose of the resources.
306            _dispose();
307            _LOGGER.info("Alerts sent.");
308        }
309    }
310    
311    /**
312     * Set the necessary request attributes
313     * @param content The content
314     */
315    protected void _setRequestAttributes (Content content)
316    {
317        Request request = ContextHelper.getRequest(_context);
318        
319        List<String> populationContexts = new ArrayList<>();
320        populationContexts.add("/application");
321        
322        request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
323    }
324    
325    /**
326     * Dispose of the resources and looked-up components.
327     */
328    protected void _dispose()
329    {
330        // Release the components.
331        if (_manager != null)
332        {
333            _manager.release(_ametysResolver);
334            _manager.release(_rightManager);
335            _manager.release(_userManager);
336        }
337        
338        _ametysResolver = null;
339        _rightManager = null;
340        _userManager = null;
341        
342        _environmentContext = null;
343        _context = null;
344        _manager = null;
345        
346        _initialized = false;
347    }
348    
349    /**
350     * Send all the alerts. Can be overridden to add alerts.
351     * @throws AmetysRepositoryException if an error occurs.
352     */
353    protected void _sendAlerts() throws AmetysRepositoryException
354    {
355        if (_instantMode)
356        {
357            _sendInstantAlerts();
358        }
359        else
360        {
361            _sendAwaitingValidationAlerts();
362            _sendUnmodifiedAlerts();
363            _sendReminders();
364            _sendScheduledArchivingReminders();
365        }
366    }
367    
368    /**
369     * Send instant alerts on contents
370     * @throws AmetysRepositoryException if an error occurred
371     */
372    protected void _sendInstantAlerts () throws AmetysRepositoryException
373    {
374        if (_instantAlertContentIds != null && !_instantAlertContentIds.isEmpty())
375        {
376            for (String contentId : _instantAlertContentIds)
377            {
378                Content content = _ametysResolver.resolveById(contentId);
379                _sendInstantAlertEmail (content, _instantAlertMessage);
380            }
381        }
382    }
383    
384    /**
385     * Send a instant e-mail alert to all the users who have the right to edit the content.
386     * @param content the content about which to send the alert.
387     * @param message the message
388     * @throws AmetysRepositoryException if an error occurred
389     */
390    protected void _sendInstantAlertEmail(Content content, String message) throws AmetysRepositoryException
391    {
392        _setRequestAttributes(content);
393        
394        Set<UserIdentity> users = new HashSet<>();
395        for (String right : _instantAlertRights)
396        {
397            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
398        }
399        
400        List<String> params = _getInstantAlertParams(content, message);
401        
402        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_instantAlertSubject, content), params);
403        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_instantAlertBody, content), params);
404        
405        String subject = _i18nUtils.translate(i18nSubject);
406        String body = _i18nUtils.translate(i18nBody);
407        
408        _sendMails(subject, body, users, _mailFrom);
409    }
410    
411    /**
412     * Send the "awaiting validation" alerts.
413     * @throws AmetysRepositoryException if an error occurs.
414     */
415    protected void _sendAwaitingValidationAlerts() throws AmetysRepositoryException
416    {
417        Long delay = Config.getInstance().getValue("remind.content.validation.delay");
418        if (delay != null && delay > 0)
419        {
420            Calendar calendar = new GregorianCalendar();
421            _removeTimeParts(calendar);
422            calendar.add(Calendar.DAY_OF_MONTH, 1 - delay.intValue());
423            
424            // No last date and X days after the proposal date.
425            Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, true));
426            Expression waitingExpression = new DateExpression("proposalDate", Operator.LT, calendar.getTime(), true);
427            // Or proposal date before the last "awaiting validation" date and the and X days after the last "awaiting validation" date.
428            Expression proposalBeforeLastDateExpr = new BinaryExpression("proposalDate", true, Operator.LT, AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, true);
429            Expression lastDateExpr = new DateExpression(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, Operator.LT, calendar.getTime(), true);
430            Expression expression = new OrExpression(new AndExpression(noLastDateExpr, waitingExpression),
431                                                     new AndExpression(proposalBeforeLastDateExpr, lastDateExpr));
432            
433            String query = ContentQueryHelper.getContentXPathQuery(expression);
434            
435            
436            
437            try (AmetysObjectIterable<ModifiableContent> contents = _ametysResolver.query(query))
438            {
439                if (_LOGGER.isInfoEnabled())
440                {
441                    _LOGGER.info("Contents waiting for validation: " + contents.getSize());
442                }
443                
444                for (ModifiableContent content : contents)
445                {
446                    // Send the alert e-mails.
447                    _sendAwaitingValidationEmail(content);
448                    
449                    // Set the last validation alert date to now.
450                    ModifiableModelLessDataHolder dataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder();
451                    dataHolder.setValue(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, ZonedDateTime.now());
452                    
453                    content.saveChanges();
454                }
455            }
456        }
457    }
458    
459    /**
460     * Send the unmodified content alerts.
461     * @throws AmetysRepositoryException if an error occurs.
462     */
463    protected void _sendUnmodifiedAlerts() throws AmetysRepositoryException
464    {
465        Long delay = Config.getInstance().getValue("remind.unmodified.content.delay");
466        if (delay != null && delay > 0)
467        {
468            Calendar calendar = new GregorianCalendar();
469            _removeTimeParts(calendar);
470            calendar.add(Calendar.DAY_OF_MONTH, 1 - delay.intValue());
471            
472            // If no step is configured, stepExpr will return the empty string.
473            Expression stepExpr = new WorkflowStepExpression(Operator.EQ, _unmodifiedContentStepIds, LogicalOperator.OR);
474            // Get only the contents on which the alert is enabled.
475            Expression unmodifiedExpr = new BooleanExpression(AlertsConstants.UNMODIFIED_ALERT_ENABLED, true, true);
476            // No last date and X days after the proposal date, or X days after the last date.
477            Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, true));
478            Expression lastModifiedExpression = new DateExpression("lastModified", Operator.LT, calendar.getTime());
479            Expression lastDateExpr = new DateExpression(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, Operator.LT, calendar.getTime(), true);
480            Expression dateExpr = new OrExpression(new AndExpression(noLastDateExpr, lastModifiedExpression), lastDateExpr);
481            // Full AND expression.
482            Expression expression = new AndExpression(unmodifiedExpr, dateExpr, stepExpr);
483            
484            String query = ContentQueryHelper.getContentXPathQuery(expression);
485            
486            try (AmetysObjectIterable<ModifiableContent> contents = _ametysResolver.query(query))
487            {
488                if (_LOGGER.isInfoEnabled())
489                {
490                    _LOGGER.info("Contents not modified for " + delay + " days: " + contents.getSize());
491                }
492                
493                for (ModifiableContent content : contents)
494                {
495                    // Send the alert e-mail.
496                    _sendUnmodifiedContentEmail(content);
497                    
498                    // Set the last unmodified alert date to now.
499                    ModifiableModelLessDataHolder dataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder();
500                    dataHolder.setValue(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, ZonedDateTime.now());
501                    
502                    content.saveChanges();
503                }
504            }
505        }
506    }
507    
508    /**
509     * Send the content reminders.
510     * @throws AmetysRepositoryException if an error occurs.
511     */
512    protected void _sendReminders() throws AmetysRepositoryException
513    {
514        Date now = DateUtils.asDate(LocalDate.now());
515        
516        Expression reminderExpr = new BooleanExpression(AlertsConstants.REMINDER_ENABLED, Operator.EQ, true, true);
517        Expression dateExpr = new DateExpression(AlertsConstants.REMINDER_DATE, Operator.EQ, now, true);
518        Expression expression = new AndExpression(reminderExpr, dateExpr);
519        
520        String query = ContentQueryHelper.getContentXPathQuery(expression);
521        
522        try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query))
523        {
524            if (_LOGGER.isInfoEnabled())
525            {
526                _LOGGER.info("Contents with reminder today: " + contents.getSize());
527            }
528            
529            for (Content content : contents)
530            {
531                _sendReminderEmail(content);
532            }
533        }
534    }
535    
536    /**
537     * Send the scheduled archiving reminders on contents.
538     * @throws AmetysRepositoryException if an error occurs.
539     */
540    protected void _sendScheduledArchivingReminders() throws AmetysRepositoryException
541    {
542        Long delay = Config.getInstance().getValue("archive.scheduler.reminder.delay");
543        if (delay != null && delay > 0)
544        {
545            Calendar calendar = new GregorianCalendar();
546            _removeTimeParts(calendar);
547            calendar.add(Calendar.DAY_OF_MONTH, delay.intValue());
548            
549            // No last date and X days before the scheduled date.
550            Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.SCHEDULED_ARCHIVING_REMINDER_LAST_DATE, true));
551            Expression scheduledDelayExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, calendar.getTime(), true);
552            Expression expression = new AndExpression(noLastDateExpr, scheduledDelayExpression);
553            
554            String query = ContentQueryHelper.getContentXPathQuery(expression);
555            
556            try (AmetysObjectIterable<ModifiableContent> contents = _ametysResolver.query(query))
557            {
558                if (_LOGGER.isInfoEnabled())
559                {
560                    _LOGGER.info("Contents with scheduled archiving reminder today: " + contents.getSize());
561                }
562                
563                for (ModifiableContent content : contents)
564                {
565                    _sendScheduledArchivingReminderEmail(content);
566                    
567                    // Set the last scheduled archiving reminder date to now.
568                    ModifiableModelLessDataHolder dataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder();
569                    dataHolder.setValue(AlertsConstants.SCHEDULED_ARCHIVING_REMINDER_LAST_DATE, ZonedDateTime.now());
570                    
571                    content.saveChanges();
572                }
573            }
574        }
575    }
576    
577    /**
578     * Send a "waiting for validation" e-mail alert to all the users who have the right to validate.
579     * @param content the content about which to send the alert.
580     * @throws AmetysRepositoryException if an error occured on the repository
581     */
582    protected void _sendAwaitingValidationEmail(Content content) throws AmetysRepositoryException
583    {
584        _setRequestAttributes(content);
585        
586        Set<UserIdentity> users = new HashSet<>();
587        for (String right : _awaitingValidationRights)
588        {
589            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
590        }
591        
592        List<String> params = _getAwaitingValidationParams(content);
593        
594        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_awaitingValidationSubject, content), params);
595        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_awaitingValidationBody, content), params);
596        
597        String subject = _i18nUtils.translate(i18nSubject);
598        String body = _i18nUtils.translate(i18nBody);
599        
600        _sendMails(subject, body, users, _mailFrom);
601    }
602    
603    /**
604     * Send a "unmodified content" e-mail alert to all the users who have the right to edit.
605     * @param content the content about which to send the alert.
606     * @throws AmetysRepositoryException if an error occured on the repository
607     */
608    protected void _sendUnmodifiedContentEmail(Content content) throws AmetysRepositoryException
609    {
610        _setRequestAttributes(content);
611        
612        Set<UserIdentity> users = new HashSet<>();
613        for (String right : _unmodifiedContentRights)
614        {
615            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
616        }
617        
618        List<String> params = _getUnmodifiedContentParams(content);
619        
620        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_unmodifiedContentSubject, content), params);
621        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_unmodifiedContentBody, content), params);
622        
623        String subject = _i18nUtils.translate(i18nSubject);
624        String body = _i18nUtils.translate(i18nBody);
625        
626        _sendMails(subject, body, users, _mailFrom);
627    }
628    
629    /**
630     * Send a reminder e-mail to all the users who have the right to edit.
631     * @param content the content about which to send the reminder.
632     * @throws AmetysRepositoryException if an error occured on the repository
633     */
634    protected void _sendReminderEmail(Content content) throws AmetysRepositoryException
635    {
636        _setRequestAttributes(content);
637        
638        Set<UserIdentity> users = new HashSet<>();
639        for (String right : _reminderRights)
640        {
641            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
642        }
643        
644        List<String> params = _getReminderParams(content);
645        
646        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_reminderSubject, content), params);
647        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_reminderBody, content), params);
648        
649        String subject = _i18nUtils.translate(i18nSubject);
650        String body = _i18nUtils.translate(i18nBody);
651        
652        _sendMails(subject, body, users, _mailFrom);
653    }
654    
655    /**
656     * Send a "scheduled archiving reminder" e-mail to all the users who have the right to archive.
657     * @param content the content about which to send the alert.
658     * @throws AmetysRepositoryException if an error occured on the repository
659     */
660    protected void _sendScheduledArchivingReminderEmail(ModifiableContent content) throws AmetysRepositoryException
661    {
662        _setRequestAttributes(content);
663        
664        Set<UserIdentity> users = new HashSet<>();
665        for (String right : _scheduledArchivingReminderRights)
666        {
667            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
668        }
669        
670        List<String> params = _getScheduledArchivingReminderParams(content);
671        
672        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_scheduledArchivingReminderSubject, content), params);
673        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_scheduledArchivingReminderBody, content), params);
674        
675        String subject = _i18nUtils.translate(i18nSubject);
676        String body = _i18nUtils.translate(i18nBody);
677        
678        _sendMails(subject, body, users, _mailFrom);
679    }
680    
681    /**
682     * Get the transform i18n body key for specific content
683     * @param bodyI18nKey the original body key
684     * @param content the content
685     * @return the transform i18n body key
686     */
687    protected String getI18nKeyBody(String bodyI18nKey, Content content)
688    {
689        return bodyI18nKey;
690    }
691    
692    /**
693     * Get the mail parameters for instant alert.
694     * @param content the content.
695     * @param message the message
696     * @return the parameters.
697     */
698    protected List<String> _getInstantAlertParams(Content content, String message)
699    {
700        List<String> params = new ArrayList<>();
701        
702        params.add(content.getTitle(null)); // {0}
703        params.add(_getContentUrl(content)); // {1}
704        params.add(message); // {2}
705        
706        params.addAll(_getAdditionalParams(content));
707        
708        return params;
709    }
710    
711    /**
712     * Get the additional i18n parameters for content
713     * @param content The content
714     * @return The additional i18n parameters
715     */
716    protected List<String> _getAdditionalParams (Content content)
717    {
718        return Collections.EMPTY_LIST;
719    }
720    
721    /**
722     * Get the mail parameters.
723     * @param content the content.
724     * @return the parameters.
725     */
726    protected List<String> _getAwaitingValidationParams(Content content)
727    {
728        List<String> params = new ArrayList<>();
729        
730        Long delay = Config.getInstance().getValue("remind.content.validation.delay");
731        
732        params.add(content.getTitle(null)); // {0}
733        params.add(_getContentUrl(content)); // {1}
734        params.add(String.valueOf(delay)); // {2}
735        
736        params.addAll(_getAdditionalParams(content));
737        
738        return params;
739    }
740    
741    /**
742     * Get the mail parameters.
743     * @param content the content.
744     * @return the parameters.
745     */
746    protected List<String> _getUnmodifiedContentParams(Content content)
747    {
748        List<String> params = new ArrayList<>();
749        
750        Long delay = Config.getInstance().getValue("remind.unmodified.content.delay");
751        
752        params.add(content.getTitle(null)); // {0}
753        params.add(_getContentUrl(content)); // {1}
754        params.add(String.valueOf(delay));  // {2}
755        
756        ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
757        String alertText = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_TEXT, StringUtils.EMPTY);
758        params.add(alertText);  // {3}
759        
760        params.addAll(_getAdditionalParams(content));
761        
762        return params;
763    }
764    
765    /**
766     * Get the mail parameters.
767     * @param content the content.
768     * @return the parameters.
769     */
770    protected List<String> _getReminderParams(Content content)
771    {
772        List<String> params = new ArrayList<>();
773        
774        // Should never trigger a ClassCastException, as we query on an unversioned metadata.
775        ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
776        String reminderText = dataHolder.getValue(AlertsConstants.REMINDER_TEXT, StringUtils.EMPTY);
777        
778        params.add(content.getTitle(null)); // {0}
779        params.add(_getContentUrl(content)); // {1}
780        params.add(reminderText);  // {2}
781        
782        params.addAll(_getAdditionalParams(content));
783        
784        return params;
785    }
786    
787    /**
788     * Get the mail parameters.
789     * @param content the content.
790     * @return the parameters.
791     */
792    protected List<String> _getScheduledArchivingReminderParams(ModifiableContent content)
793    {
794        List<String> params = new ArrayList<>();
795        
796        Long delay = Config.getInstance().getValue("archive.scheduler.reminder.delay");
797         
798        params.add(content.getTitle(null)); // {0}
799        params.add(_getContentUrl(content)); // {1}
800        params.add(String.valueOf(delay)); // {2}
801        
802        params.addAll(_getAdditionalParams(content));
803        
804        return params;
805    }
806    
807    /**
808     * Send the alert emails.
809     * @param subject the e-mail subject.
810     * @param body the e-mail body.
811     * @param users users to send the mail to.
812     * @param from the address sending the e-mail.
813     */
814    protected void _sendMails(String subject, String body, Set<UserIdentity> users, String from)
815    {
816        for (UserIdentity identity : users)
817        {
818            User user = _userManager.getUser(identity.getPopulationId(), identity.getLogin());
819            
820            if (user != null && StringUtils.isNotBlank(user.getEmail()))
821            {
822                String mail = user.getEmail();
823                
824                try
825                {
826                    SendMailHelper.sendMail(subject, null, body, mail, from);
827                }
828                catch (MessagingException e)
829                {
830                    if (_LOGGER.isWarnEnabled())
831                    {
832                        _LOGGER.warn("Could not send an alert e-mail to " + mail, e);
833                    }
834                }
835            }
836        }
837    }
838    
839    /**
840     * Get the URL to the given content tool.
841     * @param content the content.
842     * @return the content URL.
843     */
844    protected String _getContentUrl(Content content)
845    {
846        StringBuilder url = new StringBuilder(_baseUrl);
847        url.append("/index.html?uitool=uitool-content,id:%27").append(content.getId()).append("%27");
848        
849        return url.toString();
850    }
851    
852    /**
853     * Remove the time parts from a calendar, leaving only date parts.
854     * @param calendar the calendar.
855     */
856    protected void _removeTimeParts(Calendar calendar)
857    {
858        calendar.set(Calendar.HOUR_OF_DAY, 0);
859        calendar.set(Calendar.MINUTE, 0);
860        calendar.set(Calendar.SECOND, 0);
861        calendar.set(Calendar.MILLISECOND, 0);
862    }
863    
864    /**
865     * Get a set of rights from a configuration.
866     * @param configuration the configuration.
867     * @return the set of rights.
868     * @throws ConfigurationException if the configuration is not valid.
869     */
870    protected Set<String> _getRightsFromConf(Configuration configuration) throws ConfigurationException
871    {
872        Set<String> rights = new HashSet<>();
873        
874        for (Configuration rightConf : configuration.getChildren("right"))
875        {
876            String right = rightConf.getValue("");
877            if (StringUtils.isNotBlank(right))
878            {
879                rights.add(right);
880            }
881        }
882        
883        return rights;
884    }
885    
886    /**
887     * Binary date expression: test on two metadatas.
888     */
889    protected class BinaryExpression implements Expression
890    {
891        private MetadataExpression _metadata1;
892        private MetadataExpression _metadata2;
893        private Operator _operator;
894        
895        /**
896         * Creates the comparison Expression.
897         * @param metadata1 the first metadata name.
898         * @param operator the operator to make the comparison
899         * @param metadata2 the second metadata name.
900         */
901        public BinaryExpression(String metadata1, Operator operator, String metadata2)
902        {
903            _metadata1 = new MetadataExpression(metadata1);
904            _operator = operator;
905            _metadata2 = new MetadataExpression(metadata2);
906        }
907        
908        /**
909         * Creates the comparison Expression.
910         * @param metadata1 the first metadata name.
911         * @param unversioned1 true if the first metadata is unversioned.
912         * @param operator the operator to make the comparison.
913         * @param metadata2 the second metadata name.
914         * @param unversioned2 true if the second metadata is unversioned.
915         */
916        public BinaryExpression(String metadata1, boolean unversioned1, Operator operator, String metadata2, boolean unversioned2)
917        {
918            _metadata1 = new MetadataExpression(metadata1, unversioned1);
919            _operator = operator;
920            _metadata2 = new MetadataExpression(metadata2, unversioned2);
921        }
922        
923        @Override
924        public String build()
925        {
926            return _metadata1.build() + " " + _operator + " " + _metadata2.build();
927        }
928    }
929    
930}