content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.ambari.server.notifications.dispatchers;
  
  import java.util.Date;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Properties;
  import java.util.Timer;
  
  import javax.mail.AuthenticationFailedException;
  import javax.mail.Authenticator;
  import javax.mail.Message.RecipientType;
  import javax.mail.MessagingException;
  import javax.mail.NoSuchProviderException;
  import javax.mail.PasswordAuthentication;
  import javax.mail.Session;
  import javax.mail.Transport;
  import javax.mail.internet.InternetAddress;
  import javax.mail.internet.MimeMessage;
  
  import org.apache.ambari.server.notifications.DispatchCredentials;
  import org.apache.ambari.server.notifications.Notification;
  import org.apache.ambari.server.notifications.NotificationDispatcher;
  import org.apache.ambari.server.notifications.Recipient;
  import org.apache.ambari.server.notifications.TargetConfigurationResult;
  import org.apache.ambari.server.state.alert.TargetType;
  import org.apache.ambari.server.state.services.AlertNoticeDispatchService;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.inject.Singleton;
  
  /**
   * The {@link EmailDispatcher} class is used to dispatch {@link Notification}
   * via JavaMail. This class currently does not attempt to reuse an existing
   * {@link Session} or {@link Transport} since each {@link Notification} could
   * have a different target server with different properties.
   * <p/>
   * In the future, this class could keep various {@link Transport} instances open
   * on a {@link Timer}, but those instances would need to be hashed so that the
   * proper instance is retrieved from the properties of the incoming
   * {@link Notification}.
   */
  @Singleton
  public class EmailDispatcher implements NotificationDispatcher {
  
    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(EmailDispatcher.class);
  
    /**
     * The JavaMail property for the {@code From:} header.
     */
    public static final String JAVAMAIL_FROM_PROPERTY = "mail.smtp.from";
  
    /**
     * {@inheritDoc}
     */
    @Override
    public String getType() {
      return TargetType.EMAIL.name();
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isNotificationContentGenerationRequired() {
      return true;
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public void dispatch(Notification notification) {
      LOG.info("Sending email: {}", notification);
  
      if (null == notification.DispatchProperties) {
        LOG.error("Unable to dispatch an email notification that does not contain SMTP properties");
  
        if (null != notification.Callback) {
          notification.Callback.onFailure(notification.CallbackIds);
        }
  
        return;
      }
  
      // convert properties to JavaMail properties, extracting certain properties for use later
      String fromAddress = null;
      Properties properties = new Properties();
      for (Entry<String, String> entry : notification.DispatchProperties.entrySet()) {
        String key = entry.getKey();
        String value = entry.getValue();
  
        properties.put(key, value);
  
        // this is needed later on for RFC 2822 compliance when setting headers
        if (key.equals(JAVAMAIL_FROM_PROPERTY)) {
          fromAddress = value;
        }
      }
  
      // notifications must have recipients
      if (null == notification.Recipients) {
        LOG.error("Unable to dispatch an email notification that does not have recipients");
  
        if (null != notification.Callback) {
          notification.Callback.onFailure(notification.CallbackIds);
        }
  
        return;
      }
  
      // create a simple email authentication for username/password
      final Session session;
      EmailAuthenticator authenticator = null;
  
      if (null != notification.Credentials) {
        authenticator = new EmailAuthenticator(notification.Credentials);
      }
  
      session = Session.getInstance(properties, authenticator);
  
      try {
        // !!! at some point in the future we can worry about multipart
        MimeMessage message = new MimeMessage(session);
  
        for (Recipient recipient : notification.Recipients) {
          InternetAddress address = new InternetAddress(recipient.Identifier);
          message.addRecipient(RecipientType.TO, address);
        }
  
        message.setSentDate(new Date());
        message.setSubject(notification.Subject);
        message.setText(notification.Body, "UTF-8", "html");
  
        // RFC 2822 compliance
        if (null != fromAddress) {
          message.setFrom(fromAddress);
        }
  
        Transport.send(message);
  
        if (LOG.isDebugEnabled()) {
          LOG.debug("Successfully dispatched email to {}",
              notification.Recipients);
        }
  
        // callback to inform the interested parties about the successful dispatch
        if (null != notification.Callback) {
          notification.Callback.onSuccess(notification.CallbackIds);
        }
      } catch (Exception exception) {
        LOG.error("Unable to dispatch notification via Email", exception);
  
        // callback failure
        if (null != notification.Callback) {
          notification.Callback.onFailure(notification.CallbackIds);
        }
      } finally {
        try {
          session.getTransport().close();
        } catch (MessagingException me) {
          LOG.warn("Dispatcher unable to close SMTP transport", me);
        }
      }
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDigestSupported() {
      return true;
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public TargetConfigurationResult validateTargetConfig(Map<String, Object> properties) {
      try {
        Transport transport = getMailTransport(properties);
        transport.connect();
        transport.close();
      } catch(AuthenticationFailedException e) {
        LOG.debug("Invalid credentials. Authentication failure.", e);
        return TargetConfigurationResult.invalid("Invalid credentials. Authentication failure: " + e.getMessage());
      } catch(MessagingException e) {
        LOG.debug("Invalid config.", e);
        return TargetConfigurationResult.invalid("Invalid config: " + e.getMessage());
      }
      return TargetConfigurationResult.valid();
    }
  
    protected Transport getMailTransport(Map<String, Object> properties) throws NoSuchProviderException {
      DispatchCredentials credentials = null;
      if (properties.containsKey(AlertNoticeDispatchService.AMBARI_DISPATCH_CREDENTIAL_USERNAME)) {
        credentials = new DispatchCredentials();
        credentials.UserName = (String) properties.get(AlertNoticeDispatchService.AMBARI_DISPATCH_CREDENTIAL_USERNAME);
        credentials.Password = (String) properties.get(AlertNoticeDispatchService.AMBARI_DISPATCH_CREDENTIAL_PASSWORD);
      }
      Properties props = new Properties();
      for (Entry<String, Object> entry : properties.entrySet()) {
        props.put(entry.getKey(), entry.getValue());
      }
      Session session = Session.getInstance(props, new EmailAuthenticator(credentials));
      return session.getTransport();
    }
  
    /**
     * The {@link EmailAuthenticator} class is used to provide a username and
     * password combination to an SMTP server.
     */
    private static final class EmailAuthenticator extends Authenticator {
  
      private final DispatchCredentials m_credentials;
  
      /**
       * Constructor.
       *
       * @param credentials
       */
      private EmailAuthenticator(DispatchCredentials credentials) {
        m_credentials = credentials;
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      protected PasswordAuthentication getPasswordAuthentication() {
        if (m_credentials != null) {
          return new PasswordAuthentication(m_credentials.UserName,
              m_credentials.Password);
        }
        return null;
      }
    }
  }
  