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