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}