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}