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