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