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