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