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