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