/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 *      Portions copyright 2015 Edan Idzerda
 */
package org.opends.server.util;



import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.mail.MessagingException;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.opends.server.core.DirectoryServer;
import org.forgerock.i18n.slf4j.LocalizedLogger;

import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.ArgumentParser;
import com.forgerock.opendj.cli.BooleanArgument;
import com.forgerock.opendj.cli.CommonArguments;
import com.forgerock.opendj.cli.StringArgument;

import static org.opends.messages.ToolMessages.*;
import static org.opends.messages.UtilityMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;



/**
 * This class defines an e-mail message that may be sent to one or more
 * recipients via SMTP.  This is a wrapper around JavaMail to make this process
 * more convenient and fit better into the Directory Server framework.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.VOLATILE,
     mayInstantiate=true,
     mayExtend=false,
     mayInvoke=true)
public final class EMailMessage
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();


  /** The addresses of the recipients to whom this message should be sent. */
  private List<String> recipients;

  /** The set of attachments to include in this message. */
  private LinkedList<MimeBodyPart> attachments;

  /** The MIME type for the message body. */
  private String bodyMIMEType;

  /** The address of the sender for this message. */
  private String sender;

  /** The subject for the mail message. */
  private String subject;

  /** The body for the mail message. */
  private LocalizableMessageBuilder body;



  /**
   * Creates a new e-mail message with the provided information.
   *
   * @param  sender     The address of the sender for the message.
   * @param  recipient  The address of the recipient for the message.
   * @param  subject    The subject to use for the message.
   */
  public EMailMessage(String sender, String recipient, String subject)
  {
    this.sender  = sender;
    this.subject = subject;

    recipients = CollectionUtils.newArrayList(recipient);

    body         = new LocalizableMessageBuilder();
    attachments  = new LinkedList<>();
    bodyMIMEType = "text/plain";
  }



  /**
   * Creates a new e-mail message with the provided information.
   *
   * @param  sender      The address of the sender for the message.
   * @param  recipients  The addresses of the recipients for the message.
   * @param  subject     The subject to use for the message.
   */
  public EMailMessage(String sender, List<String> recipients,
                      String subject)
  {
    this.sender     = sender;
    this.recipients = recipients;
    this.subject    = subject;

    body         = new LocalizableMessageBuilder();
    attachments  = new LinkedList<>();
    bodyMIMEType = "text/plain";
  }



  /**
   * Retrieves the sender for this message.
   *
   * @return  The sender for this message.
   */
  public String getSender()
  {
    return sender;
  }



  /**
   * Specifies the sender for this message.
   *
   * @param  sender  The sender for this message.
   */
  public void setSender(String sender)
  {
    this.sender = sender;
  }



  /**
   * Retrieves the set of recipients for this message.  This list may be
   * directly manipulated by the caller.
   *
   * @return  The set of recipients for this message.
   */
  public List<String> getRecipients()
  {
    return recipients;
  }



  /**
   * Specifies the set of recipients for this message.
   *
   * @param  recipients The set of recipients for this message.
   */
  public void setRecipients(ArrayList<String> recipients)
  {
    this.recipients = recipients;
  }



  /**
   * Adds the specified recipient to this message.
   *
   * @param  recipient  The recipient to add to this message.
   */
  public void addRecipient(String recipient)
  {
    recipients.add(recipient);
  }



  /**
   * Retrieves the subject for this message.
   *
   * @return  The subject for this message.
   */
  public String getSubject()
  {
    return subject;
  }



  /**
   * Specifies the subject for this message.
   *
   * @param  subject  The subject for this message.
   */
  public void setSubject(String subject)
  {
    this.subject = subject;
  }


  /**
   * Retrieves the MIME Type for the body of this message.
   *
   * @return The MIME Type for this message.
   */
   public String getBodyMIMEType()
   {
     return bodyMIMEType;
   }

  /**
   * Specifies the MIME Type for the body of this message.
   *
   * @param bodyMIMEType  The MIME Type for this message.
   */
  public void setBodyMIMEType(String bodyMIMEType)
  {
    this.bodyMIMEType = bodyMIMEType;
  }

  /**
   * Retrieves the body for this message.  It may be directly manipulated by the
   * caller.
   *
   * @return  The body for this message.
   */
  public LocalizableMessageBuilder getBody()
  {
    return body;
  }



  /**
   * Specifies the body for this message.
   *
   * @param  body  The body for this message.
   */
  public void setBody(LocalizableMessageBuilder body)
  {
    this.body = body;
  }



  /**
   * Specifies the body for this message.
   *
   * @param  body  The body for this message.
   */
  public void setBody(LocalizableMessage body)
  {
    this.body = new LocalizableMessageBuilder(body);
  }



  /**
   * Appends the provided text to the body of this message.
   *
   * @param  text  The text to append to the body of the message.
   */
  public void appendToBody(String text)
  {
    body.append(text);
  }



  /**
   * Retrieves the set of attachments for this message.  This list may be
   * directly modified by the caller if desired.
   *
   * @return  The set of attachments for this message.
   */
  public LinkedList<MimeBodyPart> getAttachments()
  {
    return attachments;
  }



  /**
   * Adds the provided attachment to this mail message.
   *
   * @param  attachment  The attachment to add to this mail message.
   */
  public void addAttachment(MimeBodyPart attachment)
  {
    attachments.add(attachment);
  }



  /**
   * Adds an attachment to this mail message with the provided text.
   *
   * @param  attachmentText  The text to include in the attachment.
   *
   * @throws  MessagingException  If there is a problem of some type with the
   *                              attachment.
   */
  public void addAttachment(String attachmentText)
         throws MessagingException
  {
    MimeBodyPart attachment = new MimeBodyPart();
    attachment.setText(attachmentText);
    attachments.add(attachment);
  }



  /**
   * Adds the provided attachment to this mail message.
   *
   * @param  attachmentFile  The file containing the attachment data.
   *
   * @throws  MessagingException  If there is a problem of some type with the
   *                              attachment.
   */
  public void addAttachment(File attachmentFile)
         throws MessagingException
  {
    MimeBodyPart attachment = new MimeBodyPart();

    FileDataSource dataSource = new FileDataSource(attachmentFile);
    attachment.setDataHandler(new DataHandler(dataSource));
    attachment.setFileName(attachmentFile.getName());

    attachments.add(attachment);
  }



  /**
   * Attempts to send this message to the intended recipient(s).  This will use
   * the mail server(s) defined in the Directory Server mail handler
   * configuration.  If multiple servers are specified and the first is
   * unavailable, then the other server(s) will be tried before returning a
   * failure to the caller.
   *
   * @throws  MessagingException  If a problem occurred while attempting to send
   *                              the message.
   */
  public void send()
         throws MessagingException
  {
    send(DirectoryServer.getMailServerPropertySets());
  }



  /**
   * Attempts to send this message to the intended recipient(s).  If multiple
   * servers are specified and the first is unavailable, then the other
   * server(s) will be tried before returning a failure to the caller.
   *
   * @param  mailServerPropertySets  A list of property sets providing
   *                                 information about the mail servers to use
   *                                 when sending the message.
   *
   * @throws  MessagingException  If a problem occurred while attempting to send
   *                              the message.
   */
  public void send(List<Properties> mailServerPropertySets)
         throws MessagingException
  {
    // Get information about the available mail servers that we can use.
    MessagingException sendException = null;
    for (Properties props : mailServerPropertySets)
    {
      // Get a session and use it to create a new message.
      Session session = Session.getInstance(props);
      MimeMessage message = new MimeMessage(session);
      message.setSubject(subject);
      message.setSentDate(new Date());


      // Add the sender address.  If this fails, then it's a fatal problem we'll
      // propagate to the caller.
      try
      {
        message.setFrom(new InternetAddress(sender));
      }
      catch (MessagingException me)
      {
        logger.traceException(me);

        LocalizableMessage msg = ERR_EMAILMSG_INVALID_SENDER_ADDRESS.get(sender, me.getMessage());
        throw new MessagingException(msg.toString(), me);
      }


      // Add the recipient addresses.  If any of them fail, then that's a fatal
      // problem we'll propagate to the caller.
      InternetAddress[] recipientAddresses =
           new InternetAddress[recipients.size()];
      for (int i=0; i < recipientAddresses.length; i++)
      {
        String recipient = recipients.get(i);

        try
        {
          recipientAddresses[i] = new InternetAddress(recipient);
        }
        catch (MessagingException me)
        {
          logger.traceException(me);

          LocalizableMessage msg = ERR_EMAILMSG_INVALID_RECIPIENT_ADDRESS.get(recipient, me.getMessage());
          throw new MessagingException(msg.toString(), me);
        }
      }
      message.setRecipients(
              javax.mail.Message.RecipientType.TO,
              recipientAddresses);


      // If we have any attachments, then the whole thing needs to be
      // multipart.  Otherwise, just set the text of the message.
      if (attachments.isEmpty())
      {
        message.setContent(body.toString(), bodyMIMEType);
      }
      else
      {
        MimeMultipart multiPart = new MimeMultipart();

        MimeBodyPart bodyPart = new MimeBodyPart();
        bodyPart.setText(body.toString());
        multiPart.addBodyPart(bodyPart);

        for (MimeBodyPart attachment : attachments)
        {
          multiPart.addBodyPart(attachment);
        }

        message.setContent(multiPart);
      }


      // Try to send the message.  If this fails, it can be a complete failure
      // or a partial one.  If it's a complete failure then try rolling over to
      // the next server.  If it's a partial one, then that likely means that
      // the message was sent but one or more recipients was rejected, so we'll
      // propagate that back to the caller.
      try
      {
        Transport.send(message);
        return;
      }
      catch (SendFailedException sfe)
      {
        logger.traceException(sfe);

        // We'll ignore this and hope that another server is available.  If not,
        // then at least save the exception so that we can throw it if all else
        // fails.
        if (sendException == null)
        {
          sendException = sfe;
        }
      }
      // FIXME -- Are there any other types of MessagingException that we might
      //          want to catch so we could try again on another server?
    }


    // If we've gotten here, then we've tried all of the servers in the list and
    // still failed.  If we captured an earlier exception, then throw it.
    // Otherwise, throw a generic exception.
    if (sendException == null)
    {
      LocalizableMessage message = ERR_EMAILMSG_CANNOT_SEND.get();
      throw new MessagingException(message.toString());
    }
    else
    {
      throw sendException;
    }
  }



  /**
   * Provide a command-line mechanism for sending an e-mail message via SMTP.
   *
   * @param  args  The command-line arguments provided to this program.
   */
  public static void main(String[] args)
  {
    LocalizableMessage description = INFO_EMAIL_TOOL_DESCRIPTION.get();
    ArgumentParser argParser = new ArgumentParser(EMailMessage.class.getName(),
                                                  description, false);

    BooleanArgument showUsage  = null;
    StringArgument  attachFile = null;
    StringArgument  bodyFile   = null;
    StringArgument  host       = null;
    StringArgument  from       = null;
    StringArgument  subject    = null;
    StringArgument  to         = null;

    try
    {
      host = new StringArgument("host", 'h', "host", true, true, true,
                                INFO_HOST_PLACEHOLDER.get(), "127.0.0.1", null,
                                INFO_EMAIL_HOST_DESCRIPTION.get());
      argParser.addArgument(host);


      from = new StringArgument("from", 'f', "from", true, false, true,
                                INFO_ADDRESS_PLACEHOLDER.get(), null, null,
                                INFO_EMAIL_FROM_DESCRIPTION.get());
      argParser.addArgument(from);


      to = new StringArgument("to", 't', "to", true, true, true,
                              INFO_ADDRESS_PLACEHOLDER.get(),
                              null, null, INFO_EMAIL_TO_DESCRIPTION.get());
      argParser.addArgument(to);


      subject = new StringArgument("subject", 's', "subject", true, false, true,
                                   INFO_SUBJECT_PLACEHOLDER.get(), null, null,
                                   INFO_EMAIL_SUBJECT_DESCRIPTION.get());
      argParser.addArgument(subject);


      bodyFile = new StringArgument("bodyfile", 'b', "body", true, true, true,
                                    INFO_PATH_PLACEHOLDER.get(), null, null,
                                    INFO_EMAIL_BODY_DESCRIPTION.get());
      argParser.addArgument(bodyFile);


      attachFile = new StringArgument("attachfile", 'a', "attach", false, true,
                                      true, INFO_PATH_PLACEHOLDER.get(), null,
                                      null,
                                      INFO_EMAIL_ATTACH_DESCRIPTION.get());
      argParser.addArgument(attachFile);


      showUsage = CommonArguments.getShowUsage();
      argParser.addArgument(showUsage);
      argParser.setUsageArgument(showUsage);
    }
    catch (ArgumentException ae)
    {
      System.err.println(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
      System.exit(1);
    }

    try
    {
      argParser.parseArguments(args);
    }
    catch (ArgumentException ae)
    {
      argParser.displayMessageAndUsageReference(System.err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
      System.exit(1);
    }

    if (showUsage.isPresent())
    {
      return;
    }

    LinkedList<Properties> mailServerProperties = new LinkedList<>();
    for (String s : host.getValues())
    {
      Properties p = new Properties();
      p.setProperty(SMTP_PROPERTY_HOST, s);
      mailServerProperties.add(p);
    }

    EMailMessage message = new EMailMessage(from.getValue(), to.getValues(),
                                            subject.getValue());

    for (String s : bodyFile.getValues())
    {
      try
      {
        File f = new File(s);
        if (! f.exists())
        {
          System.err.println(ERR_EMAIL_NO_SUCH_BODY_FILE.get(s));
          System.exit(1);
        }

        BufferedReader reader = new BufferedReader(new FileReader(f));
        while (true)
        {
          String line = reader.readLine();
          if (line == null)
          {
            break;
          }

          message.appendToBody(line);
          message.appendToBody("\r\n"); // SMTP says we should use CRLF.
        }

        reader.close();
      }
      catch (Exception e)
      {
        System.err.println(ERR_EMAIL_CANNOT_PROCESS_BODY_FILE.get(s,
                                getExceptionMessage(e)));
        System.exit(1);
      }
    }

    if (attachFile.isPresent())
    {
      for (String s : attachFile.getValues())
      {
        File f = new File(s);
        if (! f.exists())
        {
          System.err.println(ERR_EMAIL_NO_SUCH_ATTACHMENT_FILE.get(s));
          System.exit(1);
        }

        try
        {
          message.addAttachment(f);
        }
        catch (Exception e)
        {
          System.err.println(ERR_EMAIL_CANNOT_ATTACH_FILE.get(s,
                                  getExceptionMessage(e)));
        }
      }
    }

    try
    {
      message.send(mailServerProperties);
    }
    catch (Exception e)
    {
      System.err.println(ERR_EMAIL_CANNOT_SEND_MESSAGE.get(
                              getExceptionMessage(e)));
      System.exit(1);
    }
  }
}

