001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2008 Sun Microsystems, Inc. 025 * Portions Copyright 2014-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029import static org.opends.messages.ExtensionMessages.*; 030import static org.opends.server.util.StaticUtils.*; 031 032import java.io.BufferedReader; 033import java.io.File; 034import java.io.FileReader; 035import java.util.HashMap; 036import java.util.LinkedList; 037import java.util.List; 038import java.util.Properties; 039import java.util.Set; 040 041import org.forgerock.i18n.LocalizableMessage; 042import org.forgerock.i18n.LocalizableMessageBuilder; 043import org.forgerock.i18n.slf4j.LocalizedLogger; 044import org.forgerock.opendj.config.server.ConfigException; 045import org.forgerock.opendj.ldap.ByteString; 046import org.forgerock.opendj.ldap.ResultCode; 047import org.forgerock.util.Utils; 048import org.opends.server.admin.server.ConfigurationChangeListener; 049import org.opends.server.admin.std.server.AccountStatusNotificationHandlerCfg; 050import org.opends.server.admin.std.server.SMTPAccountStatusNotificationHandlerCfg; 051import org.opends.server.api.AccountStatusNotificationHandler; 052import org.opends.server.core.DirectoryServer; 053import org.opends.server.types.AccountStatusNotification; 054import org.opends.server.types.AccountStatusNotificationProperty; 055import org.opends.server.types.AccountStatusNotificationType; 056import org.opends.server.types.Attribute; 057import org.opends.server.types.AttributeType; 058import org.forgerock.opendj.config.server.ConfigChangeResult; 059import org.opends.server.types.Entry; 060import org.opends.server.types.InitializationException; 061import org.opends.server.util.EMailMessage; 062 063/** 064 * This class provides an implementation of an account status notification 065 * handler that can send e-mail messages via SMTP to end users and/or 066 * administrators whenever an account status notification occurs. The e-mail 067 * messages will be generated from template files, which contain the information 068 * to use to create the message body. The template files may contain plain 069 * text, in addition to the following tokens: 070 * <UL> 071 * <LI>%%notification-type%% -- Will be replaced with the name of the 072 * account status notification type for the notification.</LI> 073 * <LI>%%notification-message%% -- Will be replaced with the message for the 074 * account status notification.</LI> 075 * <LI>%%notification-user-dn%% -- Will be replaced with the string 076 * representation of the DN for the user that is the target of the 077 * account status notification.</LI> 078 * <LI>%%notification-user-attr:attrname%% -- Will be replaced with the value 079 * of the attribute specified by attrname from the user's entry. If the 080 * specified attribute has multiple values, then the first value 081 * encountered will be used. If the specified attribute does not have any 082 * values, then it will be replaced with an emtpy string.</LI> 083 * <LI>%%notification-property:propname%% -- Will be replaced with the value 084 * of the specified notification property from the account status 085 * notification. If the specified property has multiple values, then the 086 * first value encountered will be used. If the specified property does 087 * not have any values, then it will be replaced with an emtpy 088 * string.</LI> 089 * </UL> 090 */ 091public class SMTPAccountStatusNotificationHandler 092 extends AccountStatusNotificationHandler 093 <SMTPAccountStatusNotificationHandlerCfg> 094 implements ConfigurationChangeListener 095 <SMTPAccountStatusNotificationHandlerCfg> 096{ 097 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 098 099 100 101 /** A mapping between the notification types and the message template. */ 102 private HashMap<AccountStatusNotificationType, 103 List<NotificationMessageTemplateElement>> templateMap; 104 105 /** A mapping between the notification types and the message subject. */ 106 private HashMap<AccountStatusNotificationType,String> subjectMap; 107 108 /** The current configuration for this account status notification handler. */ 109 private SMTPAccountStatusNotificationHandlerCfg currentConfig; 110 111 112 113 /** 114 * Creates a new, uninitialized instance of this account status notification 115 * handler. 116 */ 117 public SMTPAccountStatusNotificationHandler() 118 { 119 super(); 120 } 121 122 123 124 /** {@inheritDoc} */ 125 @Override 126 public void initializeStatusNotificationHandler( 127 SMTPAccountStatusNotificationHandlerCfg configuration) 128 throws ConfigException, InitializationException 129 { 130 currentConfig = configuration; 131 currentConfig.addSMTPChangeListener(this); 132 133 subjectMap = parseSubjects(configuration); 134 templateMap = parseTemplates(configuration); 135 136 // Make sure that the Directory Server is configured with information about 137 // one or more mail servers. 138 List<Properties> propList = DirectoryServer.getMailServerPropertySets(); 139 if (propList == null || propList.isEmpty()) 140 { 141 throw new ConfigException(ERR_SMTP_ASNH_NO_MAIL_SERVERS_CONFIGURED.get(configuration.dn())); 142 } 143 144 // Make sure that either an explicit recipient list or a set of email 145 // address attributes were provided. 146 Set<AttributeType> mailAttrs = configuration.getEmailAddressAttributeType(); 147 Set<String> recipients = configuration.getRecipientAddress(); 148 if ((mailAttrs == null || mailAttrs.isEmpty()) && 149 (recipients == null || recipients.isEmpty())) 150 { 151 throw new ConfigException(ERR_SMTP_ASNH_NO_RECIPIENTS.get(configuration.dn())); 152 } 153 } 154 155 156 157 /** 158 * Examines the provided configuration and parses the message subject 159 * information from it. 160 * 161 * @param configuration The configuration to be examined. 162 * 163 * @return A mapping between the account status notification type and the 164 * subject that should be used for messages generated for 165 * notifications with that type. 166 * 167 * @throws ConfigException If a problem occurs while parsing the subject 168 * configuration. 169 */ 170 private HashMap<AccountStatusNotificationType,String> parseSubjects( 171 SMTPAccountStatusNotificationHandlerCfg configuration) 172 throws ConfigException 173 { 174 HashMap<AccountStatusNotificationType,String> map = new HashMap<>(); 175 176 for (String s : configuration.getMessageSubject()) 177 { 178 int colonPos = s.indexOf(':'); 179 if (colonPos < 0) 180 { 181 throw new ConfigException(ERR_SMTP_ASNH_SUBJECT_NO_COLON.get(s, configuration.dn())); 182 } 183 184 String notificationTypeName = s.substring(0, colonPos).trim(); 185 AccountStatusNotificationType t = 186 AccountStatusNotificationType.typeForName(notificationTypeName); 187 if (t == null) 188 { 189 throw new ConfigException(ERR_SMTP_ASNH_SUBJECT_INVALID_NOTIFICATION_TYPE.get( 190 s, configuration.dn(), notificationTypeName)); 191 } 192 else if (map.containsKey(t)) 193 { 194 throw new ConfigException(ERR_SMTP_ASNH_SUBJECT_DUPLICATE_TYPE.get( 195 configuration.dn(), notificationTypeName)); 196 } 197 198 map.put(t, s.substring(colonPos+1).trim()); 199 if (logger.isTraceEnabled()) 200 { 201 logger.trace("Subject for notification type " + t.getName() + 202 ": " + map.get(t)); 203 } 204 } 205 206 return map; 207 } 208 209 210 211 /** 212 * Examines the provided configuration and parses the message template 213 * information from it. 214 * 215 * @param configuration The configuration to be examined. 216 * 217 * @return A mapping between the account status notification type and the 218 * template that should be used to generate messages for 219 * notifications with that type. 220 * 221 * @throws ConfigException If a problem occurs while parsing the template 222 * configuration. 223 */ 224 private HashMap<AccountStatusNotificationType, 225 List<NotificationMessageTemplateElement>> parseTemplates( 226 SMTPAccountStatusNotificationHandlerCfg configuration) 227 throws ConfigException 228 { 229 HashMap<AccountStatusNotificationType, 230 List<NotificationMessageTemplateElement>> map = new HashMap<>(); 231 232 for (String s : configuration.getMessageTemplateFile()) 233 { 234 int colonPos = s.indexOf(':'); 235 if (colonPos < 0) 236 { 237 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_NO_COLON.get(s, configuration.dn())); 238 } 239 240 String notificationTypeName = s.substring(0, colonPos).trim(); 241 AccountStatusNotificationType t = 242 AccountStatusNotificationType.typeForName(notificationTypeName); 243 if (t == null) 244 { 245 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_INVALID_NOTIFICATION_TYPE.get( 246 s, configuration.dn(), notificationTypeName)); 247 } 248 else if (map.containsKey(t)) 249 { 250 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_DUPLICATE_TYPE.get( 251 configuration.dn(), notificationTypeName)); 252 } 253 254 String path = s.substring(colonPos+1).trim(); 255 File f = new File(path); 256 if (! f.isAbsolute() ) 257 { 258 f = new File(DirectoryServer.getInstanceRoot() + File.separator + 259 path); 260 } 261 if (! f.exists()) 262 { 263 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_NO_SUCH_FILE.get( 264 path, configuration.dn())); 265 } 266 267 map.put(t, parseTemplateFile(f)); 268 if (logger.isTraceEnabled()) 269 { 270 logger.trace("Decoded template elment list for type " + 271 t.getName()); 272 } 273 } 274 275 return map; 276 } 277 278 279 280 /** 281 * Parses the specified template file into a list of notification message 282 * template elements. 283 * 284 * @param f A reference to the template file to be parsed. 285 * 286 * @return A list of notification message template elements parsed from the 287 * specified file. 288 * 289 * @throws ConfigException If error occurs while attempting to parse the 290 * template file. 291 */ 292 private List<NotificationMessageTemplateElement> parseTemplateFile(File f) 293 throws ConfigException 294 { 295 LinkedList<NotificationMessageTemplateElement> elementList = new LinkedList<>(); 296 297 BufferedReader reader = null; 298 try 299 { 300 reader = new BufferedReader(new FileReader(f)); 301 int lineNumber = 0; 302 while (true) 303 { 304 String line = reader.readLine(); 305 if (line == null) 306 { 307 break; 308 } 309 310 if (logger.isTraceEnabled()) 311 { 312 logger.trace("Read message template line " + line); 313 } 314 315 lineNumber++; 316 int startPos = 0; 317 while (startPos < line.length()) 318 { 319 int delimPos = line.indexOf("%%", startPos); 320 if (delimPos < 0) 321 { 322 if (logger.isTraceEnabled()) 323 { 324 logger.trace("No more tokens -- adding text " + 325 line.substring(startPos)); 326 } 327 328 elementList.add(new TextNotificationMessageTemplateElement( 329 line.substring(startPos))); 330 break; 331 } 332 else 333 { 334 if (delimPos > startPos) 335 { 336 if (logger.isTraceEnabled()) 337 { 338 logger.trace("Adding text before token " + 339 line.substring(startPos)); 340 } 341 342 elementList.add(new TextNotificationMessageTemplateElement( 343 line.substring(startPos, delimPos))); 344 } 345 346 int closeDelimPos = line.indexOf("%%", delimPos+1); 347 if (closeDelimPos < 0) 348 { 349 // There was an opening %% but not a closing one. 350 throw new ConfigException( 351 ERR_SMTP_ASNH_TEMPLATE_UNCLOSED_TOKEN.get( 352 delimPos, lineNumber)); 353 } 354 else 355 { 356 String tokenStr = line.substring(delimPos+2, closeDelimPos); 357 String lowerTokenStr = toLowerCase(tokenStr); 358 if (lowerTokenStr.equals("notification-type")) 359 { 360 if (logger.isTraceEnabled()) 361 { 362 logger.trace("Found a notification type token " + 363 tokenStr); 364 } 365 366 elementList.add( 367 new NotificationTypeNotificationMessageTemplateElement()); 368 } 369 else if (lowerTokenStr.equals("notification-message")) 370 { 371 if (logger.isTraceEnabled()) 372 { 373 logger.trace("Found a notification message token " + 374 tokenStr); 375 } 376 377 elementList.add( 378 new NotificationMessageNotificationMessageTemplateElement()); 379 } 380 else if (lowerTokenStr.equals("notification-user-dn")) 381 { 382 if (logger.isTraceEnabled()) 383 { 384 logger.trace("Found a notification user DN token " + 385 tokenStr); 386 } 387 388 elementList.add( 389 new UserDNNotificationMessageTemplateElement()); 390 } 391 else if (lowerTokenStr.startsWith("notification-user-attr:")) 392 { 393 String attrName = lowerTokenStr.substring(23); 394 AttributeType attrType = DirectoryServer.getAttributeType(attrName); 395 if (attrType == null) 396 { 397 throw new ConfigException( 398 ERR_SMTP_ASNH_TEMPLATE_UNDEFINED_ATTR_TYPE.get( 399 delimPos, lineNumber, attrName)); 400 } 401 else 402 { 403 if (logger.isTraceEnabled()) 404 { 405 logger.trace("Found a user attribute token for " + 406 attrType.getNameOrOID() + " -- " + 407 tokenStr); 408 } 409 410 elementList.add( 411 new UserAttributeNotificationMessageTemplateElement( 412 attrType)); 413 } 414 } 415 else if (lowerTokenStr.startsWith("notification-property:")) 416 { 417 String propertyName = lowerTokenStr.substring(22); 418 AccountStatusNotificationProperty property = 419 AccountStatusNotificationProperty.forName(propertyName); 420 if (property == null) 421 { 422 throw new ConfigException( 423 ERR_SMTP_ASNH_TEMPLATE_UNDEFINED_PROPERTY.get( 424 delimPos, lineNumber, propertyName)); 425 } 426 else 427 { 428 if (logger.isTraceEnabled()) 429 { 430 logger.trace("Found a notification property token " + 431 "for " + propertyName + " -- " + tokenStr); 432 } 433 434 elementList.add( 435 new NotificationPropertyNotificationMessageTemplateElement( 436 property)); 437 } 438 } 439 else 440 { 441 throw new ConfigException( 442 ERR_SMTP_ASNH_TEMPLATE_UNRECOGNIZED_TOKEN.get( 443 tokenStr, delimPos, lineNumber)); 444 } 445 446 startPos = closeDelimPos + 2; 447 } 448 } 449 } 450 451 452 // We need to put a CRLF at the end of the line, as per the SMTP spec. 453 elementList.add(new TextNotificationMessageTemplateElement("\r\n")); 454 } 455 456 return elementList; 457 } 458 catch (Exception e) 459 { 460 logger.traceException(e); 461 462 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_CANNOT_PARSE.get( 463 f.getAbsolutePath(), currentConfig.dn(), getExceptionMessage(e))); 464 } 465 finally 466 { 467 Utils.closeSilently(reader); 468 } 469 } 470 471 472 473 /** {@inheritDoc} */ 474 @Override 475 public boolean isConfigurationAcceptable( 476 AccountStatusNotificationHandlerCfg 477 configuration, 478 List<LocalizableMessage> unacceptableReasons) 479 { 480 SMTPAccountStatusNotificationHandlerCfg config = 481 (SMTPAccountStatusNotificationHandlerCfg) configuration; 482 return isConfigurationChangeAcceptable(config, unacceptableReasons); 483 } 484 485 486 487 /** {@inheritDoc} */ 488 @Override 489 public void handleStatusNotification(AccountStatusNotification notification) 490 { 491 SMTPAccountStatusNotificationHandlerCfg config = currentConfig; 492 HashMap<AccountStatusNotificationType,String> subjects = subjectMap; 493 HashMap<AccountStatusNotificationType, 494 List<NotificationMessageTemplateElement>> templates = templateMap; 495 496 497 // First, see if the notification type is one that we handle. If not, then 498 // return without doing anything. 499 AccountStatusNotificationType notificationType = 500 notification.getNotificationType(); 501 List<NotificationMessageTemplateElement> templateElements = 502 templates.get(notificationType); 503 if (templateElements == null) 504 { 505 if (logger.isTraceEnabled()) 506 { 507 logger.trace("No message template for notification type " + 508 notificationType.getName()); 509 } 510 511 return; 512 } 513 514 515 // It is a notification that should be handled, so we can start generating 516 // the e-mail message. First, check to see if there are any mail attributes 517 // that would cause us to send a message to the end user. 518 LinkedList<String> recipients = new LinkedList<>(); 519 Set<AttributeType> addressAttrs = config.getEmailAddressAttributeType(); 520 Set<String> recipientAddrs = config.getRecipientAddress(); 521 if (addressAttrs != null && !addressAttrs.isEmpty()) 522 { 523 Entry userEntry = notification.getUserEntry(); 524 for (AttributeType t : addressAttrs) 525 { 526 List<Attribute> attrList = userEntry.getAttribute(t); 527 if (attrList != null) 528 { 529 for (Attribute a : attrList) 530 { 531 for (ByteString v : a) 532 { 533 if (logger.isTraceEnabled()) 534 { 535 logger.trace("Adding end user recipient %s from attr %s", 536 v, a.getNameWithOptions()); 537 } 538 539 recipients.add(v.toString()); 540 } 541 } 542 } 543 } 544 545 if (recipients.isEmpty()) 546 { 547 if (recipientAddrs == null || recipientAddrs.isEmpty()) 548 { 549 // There are no recipients at all, so there's no point in generating 550 // the message. Return without doing anything. 551 if (logger.isTraceEnabled()) 552 { 553 logger.trace("No end user recipients, and no explicit " + 554 "recipients"); 555 } 556 557 return; 558 } 559 else 560 { 561 if (! config.isSendMessageWithoutEndUserAddress()) 562 { 563 // We can't send the message to the end user, and the handler is 564 // configured to not send only to administrators, so we shouln't 565 // do anything. 566 if (logger.isTraceEnabled()) 567 { 568 logger.trace("No end user recipients, and shouldn't send " + 569 "without end user recipients"); 570 } 571 572 return; 573 } 574 } 575 } 576 } 577 578 579 // Next, add any explicitly-defined recipients. 580 if (recipientAddrs != null) 581 { 582 if (logger.isTraceEnabled()) 583 { 584 for (String s : recipientAddrs) 585 { 586 logger.trace("Adding explicit recipient " + s); 587 } 588 } 589 590 recipients.addAll(recipientAddrs); 591 } 592 593 594 // Get the message subject to use. If none is defined, then use a generic 595 // subject. 596 String subject = subjects.get(notificationType); 597 if (subject == null) 598 { 599 subject = INFO_SMTP_ASNH_DEFAULT_SUBJECT.get().toString(); 600 601 if (logger.isTraceEnabled()) 602 { 603 logger.trace("Using default subject of " + subject); 604 } 605 } 606 else if (logger.isTraceEnabled()) 607 { 608 logger.trace("Using per-type subject of " + subject); 609 } 610 611 612 613 // Generate the message body. 614 LocalizableMessageBuilder messageBody = new LocalizableMessageBuilder(); 615 for (NotificationMessageTemplateElement e : templateElements) 616 { 617 e.generateValue(messageBody, notification); 618 } 619 620 621 // Create and send the e-mail message. 622 EMailMessage message = new EMailMessage(config.getSenderAddress(), 623 recipients, subject); 624 message.setBody(messageBody); 625 if (logger.isTraceEnabled()) 626 { 627 logger.trace("Set message body of " + messageBody); 628 } 629 630 631 try 632 { 633 message.send(); 634 635 if (logger.isTraceEnabled()) 636 { 637 logger.trace("Successfully sent the message"); 638 } 639 } 640 catch (Exception e) 641 { 642 logger.traceException(e); 643 644 logger.error(ERR_SMTP_ASNH_CANNOT_SEND_MESSAGE, 645 notificationType.getName(), notification.getUserDN(), getExceptionMessage(e)); 646 } 647 } 648 649 650 651 /** {@inheritDoc} */ 652 @Override 653 public boolean isConfigurationChangeAcceptable( 654 SMTPAccountStatusNotificationHandlerCfg configuration, 655 List<LocalizableMessage> unacceptableReasons) 656 { 657 boolean configAcceptable = true; 658 659 660 // Make sure that the Directory Server is configured with information about 661 // one or more mail servers. 662 List<Properties> propList = DirectoryServer.getMailServerPropertySets(); 663 if (propList == null || propList.isEmpty()) 664 { 665 unacceptableReasons.add(ERR_SMTP_ASNH_NO_MAIL_SERVERS_CONFIGURED.get(configuration.dn())); 666 configAcceptable = false; 667 } 668 669 670 // Make sure that either an explicit recipient list or a set of email 671 // address attributes were provided. 672 Set<AttributeType> mailAttrs = configuration.getEmailAddressAttributeType(); 673 Set<String> recipients = configuration.getRecipientAddress(); 674 if ((mailAttrs == null || mailAttrs.isEmpty()) && 675 (recipients == null || recipients.isEmpty())) 676 { 677 unacceptableReasons.add(ERR_SMTP_ASNH_NO_RECIPIENTS.get(configuration.dn())); 678 configAcceptable = false; 679 } 680 681 try 682 { 683 parseSubjects(configuration); 684 } 685 catch (ConfigException ce) 686 { 687 logger.traceException(ce); 688 689 unacceptableReasons.add(ce.getMessageObject()); 690 configAcceptable = false; 691 } 692 693 try 694 { 695 parseTemplates(configuration); 696 } 697 catch (ConfigException ce) 698 { 699 logger.traceException(ce); 700 701 unacceptableReasons.add(ce.getMessageObject()); 702 configAcceptable = false; 703 } 704 705 return configAcceptable; 706 } 707 708 709 710 /** {@inheritDoc} */ 711 @Override 712 public ConfigChangeResult applyConfigurationChange( 713 SMTPAccountStatusNotificationHandlerCfg configuration) 714 { 715 final ConfigChangeResult ccr = new ConfigChangeResult(); 716 try 717 { 718 HashMap<AccountStatusNotificationType,String> subjects = 719 parseSubjects(configuration); 720 HashMap<AccountStatusNotificationType, 721 List<NotificationMessageTemplateElement>> templates = 722 parseTemplates(configuration); 723 724 currentConfig = configuration; 725 subjectMap = subjects; 726 templateMap = templates; 727 } 728 catch (ConfigException ce) 729 { 730 logger.traceException(ce); 731 ccr.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 732 ccr.addMessage(ce.getMessageObject()); 733 } 734 return ccr; 735 } 736}