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