/*
 * @(#)EmailMessage.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.email;

import com.jscape.inet.mime.*;

import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.*;


/**
 * Represents an email message.
 * Used by Smtp, Pop and Imap classes to facilitate the sending and retrieving of email.
 * <p/>
 * Example Usage:<P>
 * <pre>
 * EmailMessage message = new EmailMessage();
 * message.setTo("jsmith@myserver.com");
 * message.setFrom("rjones@myserver.com");
 * message.setSubject("Meeting at 8");
 * message.setBody("See you then");
 * </pre>
 */
@SuppressWarnings("unchecked")
public class EmailMessage
        extends MimeMessage {

    private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss";

    private static final DecimalFormat OFFSET_FORMAT = new DecimalFormat("0000");
    private static final TimeZone TZ = TimeZone.getDefault();
    private static final String POSITIVE_PREFIX = "+";

    /**
     * Constructs an <code>EmailMessage</code> instance.
     * Default headers Date, MIME-Version, Message-ID, Content-Transfer-Encoding and
     * Content-Type are added automatically
     *
     * @throws MimeException if a MIME related error occurs
     */
    public EmailMessage()
            throws MimeException {
        super();
        addHeader(new MimeHeader("Date", getGMTDate()));
        addHeader(new MimeHeader("MIME-Version", "1.0"));
        addHeader(new MimeHeader("Message-ID", getMessageID()));
        addHeader(new MimeHeader("Content-Transfer-Encoding", "quoted-printable"));

        MimeHeader contentType = new MimeHeader("Content-Type", "text/plain");
        contentType.addAttribute(new MimeHeaderAttr("charset", "us-ascii"));
        addHeader(contentType);
    }

    /**
     * Constructs an <code>EmailMessage</code> instance defining to and from addresses.
     * Default headers Date, MIME-Version, Message-ID, Content-Transfer-Encoding and
     * Content-Type are added automatically.
     *
     * @param to   the to address
     * @param from the from address
     *
     * @throws MimeException if a MIME related error occurs
     */
    public EmailMessage(String to, String from)
            throws MimeException {
        this();
        addHeader(new MimeHeader("To", to));
        addHeader(new MimeHeader("From", from));
    }

    /**
     * Constructs an <code>EmailMessage</code> instance defining to and from addresses
     * and subject and body of email.
     * Default headers Date, MIME-Version, Message-ID, Content-Transfer-Encoding and
     * Content-Type are added automatically.
     *
     * @param to      the to address
     * @param from    the from address
     * @param subject the subject of the email
     * @param body    the body of the email
     *
     * @throws MimeException if a MIME related error occurs
     */
    public EmailMessage(String to, String from, String subject, String body)
            throws MimeException {
        this();
        addHeader(new MimeHeader("To", to));
        addHeader(new MimeHeader("From", from));
        addHeader(new MimeHeader("Subject", subject));
        setBody(body, StandardCharsets.UTF_8.name());
    }

    /**
     * Contructs and EmailMessage instance from contents of <code>contents</code>.
     *
     * @param contents a <code>byte[]</code> containing contents of message
     *
     * @throws MimeException if a error occurs during message parse
     */
    public EmailMessage(byte[] contents)
            throws MimeException {
        super(contents);
    }

    /**
     * Gets contents of this <code>EmailMessage</code>. Contents include all
     * MIME message headers, body and any parts.
     *
     * @return message contents
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public byte[] getMessage()
            throws MimeException {
        MimeHeader bcc = getHeader("Bcc");

        if (bcc != null) {
            MimeMessage tmp = new MimeMessage(super.getMessage());
            tmp.removeHeader("Bcc");

            return tmp.getMessage();
        } else {
            return super.getMessage();
        }
    }

    /**
     * Sets Content-Type header of email message.
     * The default content type and character set for a newly created <code>EmailMessage</code> instance.
     * are text/plain and us-ascii respectively
     *
     * @param contentType  the content type of email message
     * @param characterSet the character set used for content
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setContentType(String contentType, String characterSet)
            throws MimeException {
        MimeHeader tmpType = new MimeHeader("Content-Type", contentType);

        if (characterSet == null) {
            tmpType.addAttribute(new MimeHeaderAttr("charset", "us-ascii"));
        } else {
            tmpType.addAttribute(new MimeHeaderAttr("charset", characterSet));
        }

        addReplaceHeader(tmpType);
    }

    /**
     * Sets Content-Type header of email message.
     * The default content type and character set for a newly created <code>EmailMessage</code> instance.
     * are text/plain and us-ascii respectively
     *
     * @param contentType the content type of email message
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setContentType(String contentType)
            throws MimeException {
        setContentType(contentType, null);
    }

    /**
     * Gets Content-Type header of email message.
     *
     * @return the content type
     */
    public String getContentType() {
        MimeHeader h = getHeader("Content-Type");

        if (h == null) {
            return null;
        }

        return h.getValue();
    }

    /**
     * Adds attachment to this email message.
     * If the email message does not contain any other parts (e.g. attachments)
     * then a new part is created containing the body of the message and the current
     * value of the Content-Type and Content-Transfer-Encoding headers.  This new part
     * along with the part representing the attachment are then added to the message
     * and the Content-Type for the message is changed to multipart/mixed to indicate
     * that the message contains multiple parts.  If the message contains other parts
     * (e.g. attachments) then the body IS NOT copied and the attachment is added as a
     * new part to the message.
     * <p/>
     * Due to the way that the message body is copied when adding attachments it is
     * important that attachments be added only after the body of the message has
     * been defined.  Changing the body, Content-Type or Content-Transfer-Encoding
     * of the message after attachments have been added may result in undesired effects.
     *
     * @param attachment the <code>Attachment</code> to add
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void addAttachment(Attachment attachment)
            throws MimeException {
        addAttachment(attachment, true);
    }

    /**
     * Adds attachment to this email message.
     *
     * @param attachment the attachment to add
     * @param wrap       true if to wrap existing body in a new part and add part to message, false to leave existing body as is
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void addAttachment(Attachment attachment, boolean wrap)
            throws MimeException {
        if (wrap) {
            Enumeration parts = getParts();

            // check if there are any existing parts ... if no then get body and build part
            if (!parts.hasMoreElements()) {
                MimeHeader contentType = getHeader("Content-Type");
                MimeHeader transferEncoding = getHeader(
                        "Content-Transfer-Encoding");
                MimeMessage current = new MimeMessage();
                current.addHeader(contentType);
                current.addHeader(transferEncoding);

                byte[] tmpBody = getBodyData();
                current.setBody(tmpBody);
                addPart(current);
            }

            setBody("This is a multi-part message in MIME format.".getBytes(StandardCharsets.US_ASCII));
        }

        addPart(attachment);
    }

    /**
     * Gets all message attachments from this email message.
     * Looks for all top level parts of message that have a Content-Disposition header of type attachment
     * or inline.  In addition this method also supports attachments formatted according to the MacMIME
     * protocol specified in RFC 1740.
     *
     * @return an <code>Enumeration</code> of <code>Attachment</code>
     *
     * @throws MimeException if error occurs in decoding attachments
     */
    public Enumeration getAttachments()
            throws MimeException {
        synchronized (this) {
            Vector v = getAttachments(this);

            return v.elements();
        }
    } //end getAttachments

    /**
     * Gets attachments from message.
     *
     * @param message the message
     *
     * @return a Vector of Attachment
     *
     * @throws MimeException if a MIME parsing error occurs
     */
    private Vector getAttachments(MimeMessage message)
            throws MimeException {
        Enumeration messageParts = message.getParts();
        Vector attachments = new Vector();

        // message has parts ... enumerate
        if (messageParts.hasMoreElements()) {
            while (messageParts.hasMoreElements()) {
                MimeMessage part = (MimeMessage) messageParts.nextElement();

                // check if part is an attachment
                Attachment a = getAttachment(part);

                if (a != null) {
                    attachments.add(a);
                }
            }
        } else {
            // check if this message is an attachment
            Attachment a = getAttachment(message);

            if (a != null) {
                attachments.add(a);
            }
        }

        return attachments;
    }

    /**
     * Gets attachment from MimeMessage part.
     *
     * @param part the part to obtain the attachment from
     *
     * @return an Attachment
     *
     * @throws MimeException if a MIME parsing error occurs
     */
    private Attachment getAttachment(MimeMessage part)
            throws MimeException {
        MimeHeader type = part.getHeader("Content-Type");

        if (type != null) {
            String typeValue = type.getValue().toLowerCase(Locale.ENGLISH);
            // special handling for MacMIME attachments per RFC 1740
            if (typeValue.equals("multipart/appledouble")) {
                Enumeration subparts = part.getParts();

                while (subparts.hasMoreElements()) {
                    MimeMessage subpart = (MimeMessage) subparts.nextElement();
                    MimeHeader sct = subpart.getHeader("Content-Type");
                    MimeHeader sHead = subpart.getHeader("Content-Disposition");

                    if (sct != null) {
                        // found attachment
                        if (!sct.getValue().toLowerCase(Locale.ENGLISH)
                                .equals("application/applefile")) {
                            MimeHeaderAttr fileNameAttr = null;
                            String filename = null;

                            if (sHead != null) {
                                fileNameAttr = sHead.getAttribute("filename");
                            }

                            // get filename for attachment
                            if (fileNameAttr != null) {
                                filename = fileNameAttr.getValue();
                            } else {
                                // attempt to get filename from Content-Type header
                                MimeHeaderAttr ctn = sct.getAttribute("name");

                                if (ctn != null) {
                                    filename = ctn.getValue();
                                }
                            }

                            byte[] filedata = subpart.getBodyData(); // auto-decoded by API

                            if (filename != null) {
                                return new Attachment(filename, filedata);
                            } else {
                                return new Attachment(filedata);
                            }
                        }
                    }
                }
            } else if (typeValue.equals("message/rfc822")) {
                // handle embedded messages
                return new Attachment(part);
            } else if (typeValue.equals("application/octet-stream")
                    || (typeValue.startsWith("application/") && type.getAttribute("name") != null)) {
                // handle regular attachment as per RFC 1521 - application/octet-stream
                // and deprecated use of "NAME" parameter without Content-Disposition header
                return new Attachment(part);
            }
        }

        MimeHeader disposition = part.getHeader("Content-Disposition");

        if (disposition != null) {
            if (disposition.getValue().toLowerCase(Locale.ENGLISH).equals("attachment")
                    || (disposition.getValue().toLowerCase(Locale.ENGLISH).equals("inline")
                    && disposition.getAttribute("filename") != null)) {
                return new Attachment(part);
            }
        }

        return null;
    }

    /**
     * Sets from email address.
     *
     * @param from the from email address
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setFrom(String from)
            throws MimeException {
        MimeHeader head = new MimeHeader("From", from);
        addReplaceHeader(head);
    }

    /**
     * Sets from email address using defined character encoding.
     * From address is encoded according to RFC 2047.
     * This method should be used in cases where the address contains data other than US-ASCII characters.
     *
     * @param from              the from email address
     * @param characterEncoding the character encoding used
     *
     * @throws MimeException thrown if an error occurs while encoding From header
     * @see com.jscape.inet.mime.MimeException
     */
    public void setFrom(String from, String characterEncoding)
            throws MimeException {
        MimeHeader head = new MimeHeader("From", from, characterEncoding);
        addReplaceHeader(head);
    }

    /**
     * Sets receipient to email address.  To send to multiple recipients pass in all recipients comma delimited.
     * Example: user@domain.com,user2@domain.com,user3@domain.com
     *
     * @param to the recipient to email address
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setTo(String to)
            throws MimeException {
        MimeHeader head = new MimeHeader("To", to);
        addReplaceHeader(head);
    }

    /**
     * Sets receipient to email address.  To send to multiple recipients pass in all recipients comma space delimited.
     * Example:
     * <pre>user&#64;domain.com, user2&#64;domain.com, user3&#64;domain.com</pre>
     * To address is encoded according to RFC 2047.
     * This method should be used in cases where the address contains data other than US-ASCII characters.
     *
     * @param to                the to email address
     * @param characterEncoding the character encoding used
     *
     * @throws MimeException thrown if an error occurs while encoding To header
     * @see com.jscape.inet.mime.MimeException
     */
    public void setTo(String to, String characterEncoding)
            throws MimeException {
        MimeHeader head = new MimeHeader("To", to, characterEncoding);
        addReplaceHeader(head);
    }

    /**
     * Sets receipient carbon-copy email address. To send to multiple recipients pass in all recipients comma delimited.
     * Example:
     * <pre>user&#64;domain.com, user2&#64;domain.com, user3&#64;domain.com</pre>
     *
     * @param cc the recipient carbon-copy email address
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setCc(String cc)
            throws MimeException {
        MimeHeader head = new MimeHeader("Cc", cc);
        addReplaceHeader(head);
    }

    /**
     * Sets receipient carbon-copy email address. To send to multiple recipients pass in all recipients comma delimited.
     * Example:
     * <pre>user&#64;domain.com, user2&#64;domain.com, user3&#64;domain.com</pre>
     * Cc address is encoded according to RFC 2047.
     * This method should be used in cases where the address contains data other than US-ASCII characters.
     *
     * @param cc                the recipient carbon-copy email address
     * @param characterEncoding the character encoding used
     *
     * @throws MimeException thrown if an error occurs while encoding Cc header
     * @see com.jscape.inet.mime.MimeException
     */
    public void setCc(String cc, String characterEncoding)
            throws MimeException {
        MimeHeader head = new MimeHeader("Cc", cc, characterEncoding);
        addReplaceHeader(head);
    }

    /**
     * Sets receipient blind-carbon-copy email address. To send to multiple recipients pass in all recipients comma delimited.
     * Example:
     * <pre>user&#64;domain.com,user2&#64;domain.com,user3&#64;domain.com</pre>
     *
     * @param bcc the recipient blind-carbon-copy email address
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setBcc(String bcc)
            throws MimeException {
        MimeHeader head = new MimeHeader("Bcc", bcc);
        addReplaceHeader(head);
    }

    /**
     * Sets message Reply-To address.
     *
     * @param replyto           the reply-to email address
     * @param characterEncoding the character encoding to use when setting the Reply-To address
     *
     * @throws MimeException thrown if an error occurs while encoding Reply-To header
     * @see com.jscape.inet.mime.MimeException
     */
    public void setReplyTo(String replyto, String characterEncoding)
            throws MimeException {
        MimeHeader head = new MimeHeader("Reply-To", replyto, characterEncoding);
        addReplaceHeader(head);
    }

    /**
     * Sets message Reply-To address.
     *
     * @param replyto the reply-to email address
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setReplyTo(String replyto)
            throws MimeException {
        MimeHeader head = new MimeHeader("Reply-To", replyto);
        addReplaceHeader(head);
    }

    /**
     * Sets message subject.
     *
     * @param subject the subject of message
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setSubject(String subject)
            throws MimeException {
        MimeHeader head = new MimeHeader("Subject", subject);
        addReplaceHeader(head);
    }

    /**
     * Sets message subject.
     *
     * @param subject           the subject of message
     * @param characterEncoding the character encoding used when setting the Subject header
     *
     * @throws MimeException if a MIME related error occurs
     */
    public void setSubject(String subject, String characterEncoding)
            throws MimeException {
        MimeHeader head = new MimeHeader("Subject", subject, characterEncoding);
        addReplaceHeader(head);
    }

    /**
     * Gets Subject of message.
     *
     * @return the subject or <code>null</code> if no Subject header is present
     */
    public String getSubject() {
        MimeHeader h = getHeader("Subject");

        if (h == null) {
            return null;
        }

        return h.getValue();
    }

    /**
     * Gets From address of message.
     *
     * @return the from address or null of no From header is present
     */
    public String getFrom() {
        MimeHeader h = getHeader("From");

        if (h == null) {
            return null;
        }

        return h.getValue();
    }

    /**
     * Gets To address of message.
     *
     * @return the to address or null of no To header is present
     */
    public String getTo() {
        MimeHeader h = getHeader("To");

        if (h == null) {
            return null;
        }

        return h.getValue();
    }

    /**
     * Gets Cc address of message.
     *
     * @return the carbon-copy address or null of no Cc header is present
     */
    public String getCc() {
        MimeHeader h = getHeader("Cc");

        if (h == null) {
            return null;
        }

        return h.getValue();
    }

    /**
     * Gets Bcc address of message.
     *
     * @return the blind-carbon-copy address or null of no Bcc header is present
     */
    public String getBcc() {
        MimeHeader h = getHeader("Bcc");

        if (h == null) {
            return null;
        }

        return h.getValue();
    }

    /**
     * Gets Date of message.
     * According to RFC 822 the Date header may come back in either of the following formats:
     * <p/>
     * EEE, d MMM yyyy HH:mm:ss z
     * <p/>
     * The day of week portion denoted as EEE is optional.  Examples of formats include:
     * <p/>
     * Date: Fri, 16 May 2003 10:01:14 -0700
     * The above date includes the day of week.
     * <p/>
     * Date: 16 May 2003 10:01:14 -0700
     * The above date does not include the day of week.
     *
     * @return the message date or null of no Date header is present
     */
    public String getDate() {
        MimeHeader h = getHeader("Date");

        if (h == null) {
            return null;
        }

        return h.getValue();
    }

    /**
     * Gets reply-to address of message.
     *
     * @return the reply-to address or null of no Reply-To header is present
     */
    public String getReplyTo() {
        MimeHeader h = getHeader("Reply-To");

        if (h == null) {
            return null;
        }

        return h.getValue();
    }

    /**
     * Gets body of message.
     * If message has Content-Type header of multipart/mixed or multipart/alternative then the body
     * returned will be that of the first message part that has a Content-Type header that is not multipart/mixed
     * or multipart/alternative.
     *
     * @return the message body or null of no body is present
     *
     * @throws MimeException if a MIME related error occurs
     */
    public String getBody()
            throws MimeException {
        String body = null;
        String contentType = getContentType();

        if (contentType != null
                && (contentType.equalsIgnoreCase("multipart/mixed")
                || contentType.equalsIgnoreCase("multipart/alternative")
                || contentType.equalsIgnoreCase("multipart/related"))) {
            body = getMultipartBody(this);
        } else {
            body = super.getBody();
        }

        return body;
    }

    /**
     * Gets body of multipart/mixed or multipart/alternative message as would be seen by a text only email
     * client.
     *
     * @param m the message to retrieve from
     *
     * @return the body
     *
     * @throws MimeException if a MIME related error occurs
     */
    private String getMultipartBody(MimeMessage m)
            throws MimeException {
        String body = null;
        Enumeration p = m.getParts();

        // check if does not have any parts 
        if (!p.hasMoreElements()) {
            body = super.getBody();
        }

        while (p.hasMoreElements()) {
            MimeMessage message = (MimeMessage) p.nextElement();
            String contentType = message.getContentType();

            if (contentType == null) {
                body = message.getBody();

                break;
            } else if (contentType.equalsIgnoreCase("multipart/mixed")
                    || contentType.equalsIgnoreCase("multipart/alternative")
                    || contentType.equalsIgnoreCase("multipart/related")) {
                body = getMultipartBody(message);

                break;
            } else {
                body = message.getBody();

                break;
            }
        }

        return body;
    }

    /**
     * Generates a unique message id to be used in Message-ID header.
     *
     * @return a message id
     */
    private String getMessageID() {
        return "<2.4." + UUID.randomUUID().toString().replace('-', '.') + "@localhost>";
    }

    /**
     * Gets Date in RFC822 format.
     *
     * @return the current date/time formatted according to RFC822
     */
    private String getGMTDate() {
        OFFSET_FORMAT.setPositivePrefix(POSITIVE_PREFIX);

        Calendar cd = Calendar.getInstance(Locale.ENGLISH);

        String offset = (OFFSET_FORMAT.format(TZ.getOffset(cd.get(Calendar.ERA),
                                                           cd.get(Calendar.YEAR), cd.get(Calendar.MONTH),
                                                           cd.get(Calendar.DAY_OF_MONTH),
                                                           cd.get(Calendar.DAY_OF_WEEK), cd.get(Calendar.MILLISECOND)) / 36000));

        return new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH).format(new Date()) + " " + offset;
    }

    /**
     * Sets priority of message when displayed in recipients email client.
     * 1 = High priority, 3 = Normal priority and 5 = Low priority.
     *
     * @param priority the message priority
     *
     * @throws MimeException if a MIME related error occurs while reading message
     */
    public void setPriority(int priority)
            throws MimeException {
        switch (priority) {
            case 1:
                addReplaceHeader("X-Priority", "1");
                addReplaceHeader("X-MSMail-Priority", "High");

                break;

            case 3:
                addReplaceHeader("X-Priority", "3");
                addReplaceHeader("X-MSMail-Priority", "Normal");

                break;

            case 5:
                addReplaceHeader("X-Priority", "5");
                addReplaceHeader("X-MSMail-Priority", "Low");

                break;

            default:
                break;
        }
    }

    /**
     * Gets priority of message when displayed in recipients email client.
     * 1 = High priority, 3 = Normal priority and 5 = Low priority.
     *
     * @return the message priority
     *
     * @throws MimeException if a MIME related error occurs while reading message
     */
    public int getPriority()
            throws MimeException {
        MimeHeader xp = getHeader("X-Priority");
        int priority = 3;

        // if X-Priority header not found
        if (xp == null) {
            MimeHeader xms = getHeader("X-MSMail-Priority");

            if (xms != null) {
                String value = xms.getValue();

                if (value.equalsIgnoreCase("High")) {
                    priority = 1;
                } else if (value.equalsIgnoreCase("Normal")) {
                    priority = 3;
                } else if (value.equalsIgnoreCase("Low")) {
                    priority = 5;
                } else {
                    priority = 3;
                }
            } else {
                priority = 3;
            }
        } else {
            // X-Priority header found
            try {
                // get first token from header value
                StringTokenizer st = new StringTokenizer(xp.getValue());
                priority = Integer.parseInt(st.nextToken());
            } catch (Exception e) {
                priority = 3;
            }
        }

        return priority;
    }
}
