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}