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