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