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