/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.core.net;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Properties;

import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

import javax.naming.Context;
import javax.naming.InitialContext;

import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.boolex.EvaluationException;
import ch.qos.logback.core.boolex.EventEvaluator;
import ch.qos.logback.core.helpers.CyclicBuffer;
import ch.qos.logback.core.pattern.PatternLayoutBase;
import ch.qos.logback.core.sift.DefaultDiscriminator;
import ch.qos.logback.core.sift.Discriminator;
import ch.qos.logback.core.spi.CyclicBufferTracker;
import ch.qos.logback.core.util.ContentTypeUtil;
import ch.qos.logback.core.util.OptionHelper;

// Contributors:
// Andrey Rybin charset encoding support http://jira.qos.ch/browse/LBCORE-69

/**
 * An abstract class that provides support for sending events to an email address.
 * <p></p>
 * <p></p>
 * See http://logback.qos.ch/manual/appenders.html#SMTPAppender for further documentation.
 * 
 * @author Ceki G&uuml;lc&uuml;
 * @author S&eacute;bastien Pennec
 */
public abstract class SMTPAppenderBase<E> extends AppenderBase<E> {
    static InternetAddress[] EMPTY_IA_ARRAY = new InternetAddress[0];

    // ~ 14 days
    static final int MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND;
    long lastTrackerStatusPrint = 0;
    int delayBetweenStatusMessages = 300 * CoreConstants.MILLIS_IN_ONE_SECOND;
    protected Layout<E> subjectLayout;
    protected Layout<E> layout;
    private List<PatternLayoutBase<E>> toPatternLayoutList = new ArrayList<PatternLayoutBase<E>>();
    private String from;
    private String subjectStr = null;
    private String smtpHost;
    private int smtpPort = 25;
    private boolean starttls = false;
    private boolean ssl = false;
    private boolean sessionViaJNDI = false;
    private String jndiLocation = CoreConstants.JNDI_COMP_PREFIX
            + "/mail/Session";
    String username;
    String password;
    String localhost;
    boolean asynchronousSending = true;
    private String charsetEncoding = "UTF-8";
    protected Session session;
    protected EventEvaluator<E> eventEvaluator;
    protected Discriminator<E> discriminator = new DefaultDiscriminator<E>();
    protected CyclicBufferTracker<E> cbTracker;
    private int errorCount = 0;

    /**
     * return a layout for the subject string as appropriate for the module. If the subjectStr parameter is null, then a
     * default value for subjectStr should be used.
     * 
     * @param subjectStr
     * @return a layout as appropriate for the module
     */
    protected abstract Layout<E> makeSubjectLayout(String subjectStr);

    /**
     * Start the appender
     */
    public void start() {
        if (cbTracker == null) {
            cbTracker = new CyclicBufferTracker<E>();
        }

        if (sessionViaJNDI) {
            session = lookupSessionInJNDI();
        } else {
            session = buildSessionFromProperties();
        }

        if (session == null) {
            addError("Failed to obtain javax.mail.Session. Cannot start.");

            return;
        }

        subjectLayout = makeSubjectLayout(subjectStr);

        started = true;
    }

    private Session lookupSessionInJNDI() {
        addInfo("Looking up javax.mail.Session at JNDI location ["
                + jndiLocation + "]");

        try {
            Context initialContext = new InitialContext();
            Object obj = initialContext.lookup(jndiLocation);

            return (Session) obj;
        } catch (Exception e) {
            addError("Failed to obtain javax.mail.Session from JNDI location ["
                    + jndiLocation + "]");

            return null;
        }
    }

    private Session buildSessionFromProperties() {
        Properties props = new Properties(OptionHelper.getSystemProperties());

        if (smtpHost != null) {
            props.put("mail.smtp.host", smtpHost);
        }

        props.put("mail.smtp.port", Integer.toString(smtpPort));

        if (localhost != null) {
            props.put("mail.smtp.localhost", localhost);
        }

        LoginAuthenticator loginAuthenticator = null;

        if (username != null) {
            loginAuthenticator = new LoginAuthenticator(username, password);
            props.put("mail.smtp.auth", "true");
        }

        if (isSTARTTLS() && isSSL()) {
            addError("Both SSL and StartTLS cannot be enabled simultaneously");
        } else {
            if (isSTARTTLS()) {
                // see also http://jira.qos.ch/browse/LBCORE-225
                props.put("mail.smtp.starttls.enable", "true");
            }

            if (isSSL()) {
                String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
                props.put("mail.smtp.socketFactory.port",
                        Integer.toString(smtpPort));
                props.put("mail.smtp.socketFactory.class", SSL_FACTORY);
                props.put("mail.smtp.socketFactory.fallback", "true");
                props.put("mail.smtp.ssl.enable", "true");
                props.put("mail.transport.protocol", "smtps");
                props.put("mail.smtps.ssl.trust", "*");
            }
        }

        // props.put("mail.debug", "true");
        return Session.getInstance(props, loginAuthenticator);
    }

    /**
     * Perform SMTPAppender specific appending actions, delegating some of them to a subclass and checking if the event
     * triggers an e-mail to be sent.
     */
    protected void append(E eventObject) {
        if (!checkEntryConditions()) {
            return;
        }

        String key = discriminator.getDiscriminatingValue(eventObject);
        long now = System.currentTimeMillis();
        final CyclicBuffer<E> cb = cbTracker.getOrCreate(key, now);
        subAppend(cb, eventObject);

        try {
            if (eventEvaluator.evaluate(eventObject)) {
                // clone the CyclicBuffer before sending out asynchronously
                CyclicBuffer<E> cbClone = new CyclicBuffer<E>(cb);
                // see http://jira.qos.ch/browse/LBCLASSIC-221
                cb.clear();

                if (asynchronousSending) {
                    // perform actual sending asynchronously
                    SenderRunnable senderRunnable = new SenderRunnable(cbClone,
                            eventObject);
                    context.getExecutorService().execute(senderRunnable);
                } else {
                    // synchronous sending
                    sendBuffer(cbClone, eventObject);
                }
            }
        } catch (EvaluationException ex) {
            errorCount++;

            if (errorCount < CoreConstants.MAX_ERROR_COUNT) {
                addError("SMTPAppender's EventEvaluator threw an Exception-",
                        ex);
            }
        }

        // immediately remove the buffer if asked by the user
        if (eventMarksEndOfLife(eventObject)) {
            cbTracker.endOfLife(key);
        }

        cbTracker.removeStaleComponents(now);

        if ((lastTrackerStatusPrint + delayBetweenStatusMessages) < now) {
            addInfo("SMTPAppender [" + name + "] is tracking ["
                    + cbTracker.getComponentCount() + "] buffers");
            lastTrackerStatusPrint = now;

            // quadruple 'delay' assuming less than max delay
            if (delayBetweenStatusMessages < MAX_DELAY_BETWEEN_STATUS_MESSAGES) {
                delayBetweenStatusMessages *= 4;
            }
        }
    }

    protected abstract boolean eventMarksEndOfLife(E eventObject);

    protected abstract void subAppend(CyclicBuffer<E> cb, E eventObject);

    /**
     * This method determines if there is a sense in attempting to append.
     * <p></p>
     * <p></p>
     * It checks whether there is a set output target and also if there is a set layout. If these checks fail, then the
     * boolean value <code>false</code> is returned.
     */
    public boolean checkEntryConditions() {
        if (!this.started) {
            addError("Attempting to append to a non-started appender: "
                    + this.getName());

            return false;
        }

        if (this.eventEvaluator == null) {
            addError("No EventEvaluator is set for appender [" + name + "].");

            return false;
        }

        if (this.layout == null) {
            addError("No layout set for appender named ["
                    + name
                    + "]. For more information, please visit http://logback.qos.ch/codes.html#smtp_no_layout");

            return false;
        }

        return true;
    }

    public synchronized void stop() {
        this.started = false;
    }

    InternetAddress getAddress(String addressStr) {
        try {
            return new InternetAddress(addressStr);
        } catch (AddressException e) {
            addError("Could not parse address [" + addressStr + "].", e);

            return null;
        }
    }

    private List<InternetAddress> parseAddress(E event) {
        int len = toPatternLayoutList.size();

        List<InternetAddress> iaList = new ArrayList<InternetAddress>();

        for (int i = 0; i < len; i++) {
            try {
                PatternLayoutBase<E> emailPL = toPatternLayoutList.get(i);
                String emailAdrr = emailPL.doLayout(event);

                if ((emailAdrr == null) || (emailAdrr.length() == 0)) {
                    continue;
                }

                InternetAddress[] tmp = InternetAddress.parse(emailAdrr, true);
                iaList.addAll(Arrays.asList(tmp));
            } catch (AddressException e) {
                addError("Could not parse email address for ["
                        + toPatternLayoutList.get(i) + "] for event [" + event
                        + "]", e);

                return iaList;
            }
        }

        return iaList;
    }

    /**
     * Returns value of the <b>toList</b> option.
     */
    public List<PatternLayoutBase<E>> getToList() {
        return toPatternLayoutList;
    }

    /**
     * Send the contents of the cyclic buffer as an e-mail message.
     */
    protected void sendBuffer(CyclicBuffer<E> cb, E lastEventObject) {
        // Note: this code already owns the monitor for this
        // appender. This frees us from needing to synchronize on 'cb'.
        try {
            MimeBodyPart part = new MimeBodyPart();

            StringBuffer sbuf = new StringBuffer();

            String header = layout.getFileHeader();

            if (header != null) {
                sbuf.append(header);
            }

            String presentationHeader = layout.getPresentationHeader();

            if (presentationHeader != null) {
                sbuf.append(presentationHeader);
            }

            fillBuffer(cb, sbuf);

            String presentationFooter = layout.getPresentationFooter();

            if (presentationFooter != null) {
                sbuf.append(presentationFooter);
            }

            String footer = layout.getFileFooter();

            if (footer != null) {
                sbuf.append(footer);
            }

            String subjectStr = "Undefined subject";

            if (subjectLayout != null) {
                subjectStr = subjectLayout.doLayout(lastEventObject);

                // The subject must not contain new-line characters, which cause
                // an SMTP error (LOGBACK-865). Truncate the string at the first
                // new-line character.
                int newLinePos = (subjectStr != null) ? subjectStr
                        .indexOf('\n') : (-1);

                if (newLinePos > -1) {
                    subjectStr = subjectStr.substring(0, newLinePos);
                }
            }

            MimeMessage mimeMsg = new MimeMessage(session);

            if (from != null) {
                mimeMsg.setFrom(getAddress(from));
            } else {
                mimeMsg.setFrom();
            }

            mimeMsg.setSubject(subjectStr, charsetEncoding);

            List<InternetAddress> destinationAddresses = parseAddress(lastEventObject);

            if (destinationAddresses.isEmpty()) {
                addInfo("Empty destination address. Aborting email transmission");

                return;
            }

            InternetAddress[] toAddressArray = destinationAddresses
                    .toArray(EMPTY_IA_ARRAY);
            mimeMsg.setRecipients(Message.RecipientType.TO, toAddressArray);

            String contentType = layout.getContentType();

            if (ContentTypeUtil.isTextual(contentType)) {
                part.setText(sbuf.toString(), charsetEncoding,
                        ContentTypeUtil.getSubType(contentType));
            } else {
                part.setContent(sbuf.toString(), layout.getContentType());
            }

            Multipart mp = new MimeMultipart();
            mp.addBodyPart(part);
            mimeMsg.setContent(mp);

            mimeMsg.setSentDate(new Date());
            addInfo("About to send out SMTP message \"" + subjectStr + "\" to "
                    + Arrays.toString(toAddressArray));

            // Transport.send(mimeMsg);
            Transport transport = session.getTransport(session
                    .getProperty("mail.transport.protocol"));
            transport.connect(smtpHost, smtpPort, username, password);
            transport.sendMessage(mimeMsg, mimeMsg.getAllRecipients());
            transport.close();
        } catch (Exception e) {
            addError("Error occurred while sending e-mail notification.", e);
        }
    }

    protected abstract void fillBuffer(CyclicBuffer<E> cb, StringBuffer sbuf);

    /**
     * Returns value of the <b>From</b> option.
     */
    public String getFrom() {
        return from;
    }

    /**
     * Returns value of the <b>Subject</b> option.
     */
    public String getSubject() {
        return subjectStr;
    }

    /**
     * The <b>From</b> option takes a string value which should be a e-mail address of the sender.
     */
    public void setFrom(String from) {
        this.from = from;
    }

    /**
     * The <b>Subject</b> option takes a string value which should be a the subject of the e-mail message.
     */
    public void setSubject(String subject) {
        this.subjectStr = subject;
    }

    /**
     * Alias for smtpHost
     * 
     * @param smtpHost
     */
    public void setSMTPHost(String smtpHost) {
        setSmtpHost(smtpHost);
    }

    /**
     * The <b>smtpHost</b> option takes a string value which should be a the host name of the SMTP server that will send
     * the e-mail message.
     */
    public void setSmtpHost(String smtpHost) {
        this.smtpHost = smtpHost;
    }

    /**
     * Alias for getSmtpHost().
     */
    public String getSMTPHost() {
        return getSmtpHost();
    }

    /**
     * Returns value of the <b>SMTPHost</b> option.
     */
    public String getSmtpHost() {
        return smtpHost;
    }

    /**
     * Alias for {@link #setSmtpPort}.
     * 
     * @param port
     */
    public void setSMTPPort(int port) {
        setSmtpPort(port);
    }

    /**
     * The port where the SMTP server is running. Default value is 25.
     * 
     * @param port
     */
    public void setSmtpPort(int port) {
        this.smtpPort = port;
    }

    /**
     * Alias for {@link #getSmtpPort}
     * 
     * @return
     */
    public int getSMTPPort() {
        return getSmtpPort();
    }

    /**
     * See {@link #setSmtpPort}
     * 
     * @return
     */
    public int getSmtpPort() {
        return smtpPort;
    }

    public String getLocalhost() {
        return localhost;
    }

    /**
     * Set the "mail.smtp.localhost" property to the value passed as parameter to this method.
     * <p></p>
     * <p>
     * Useful in case the hostname for the client host is not fully qualified and as a consequence the SMTP server
     * rejects the clients HELO/EHLO command.
     * </p>
     * 
     * @param localhost
     */
    public void setLocalhost(String localhost) {
        this.localhost = localhost;
    }

    public CyclicBufferTracker<E> getCyclicBufferTracker() {
        return cbTracker;
    }

    public void setCyclicBufferTracker(CyclicBufferTracker<E> cbTracker) {
        this.cbTracker = cbTracker;
    }

    public Discriminator<E> getDiscriminator() {
        return discriminator;
    }

    public void setDiscriminator(Discriminator<E> discriminator) {
        this.discriminator = discriminator;
    }

    public boolean isAsynchronousSending() {
        return asynchronousSending;
    }

    /**
     * By default, SMTAppender transmits emails asynchronously. For synchronous email transmission set
     * asynchronousSending to 'false'.
     * 
     * @param asynchronousSending
     *            determines whether sending is done asynchronously or not
     * @since 1.0.4
     */
    public void setAsynchronousSending(boolean asynchronousSending) {
        this.asynchronousSending = asynchronousSending;
    }

    public void addTo(String to) {
        if ((to == null) || (to.length() == 0)) {
            throw new IllegalArgumentException("Null or empty <to> property");
        }

        PatternLayoutBase plb = makeNewToPatternLayout(to.trim());
        plb.setContext(context);
        plb.start();
        this.toPatternLayoutList.add(plb);
    }

    protected abstract PatternLayoutBase<E> makeNewToPatternLayout(
            String toPattern);

    public List<String> getToAsListOfString() {
        List<String> toList = new ArrayList<String>();

        for (PatternLayoutBase plb : toPatternLayoutList) {
            toList.add(plb.getPattern());
        }

        return toList;
    }

    public boolean isSTARTTLS() {
        return starttls;
    }

    public void setSTARTTLS(boolean startTLS) {
        this.starttls = startTLS;
    }

    public boolean isSSL() {
        return ssl;
    }

    public void setSSL(boolean ssl) {
        this.ssl = ssl;
    }

    /**
     * The <b>EventEvaluator</b> option takes a string value representing the name of the class implementing the
     * {@link EventEvaluator} interface. A corresponding object will be instantiated and assigned as the event evaluator
     * for the SMTPAppender.
     */
    public void setEvaluator(EventEvaluator<E> eventEvaluator) {
        this.eventEvaluator = eventEvaluator;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * @return the charset encoding value
     * @see #setCharsetEncoding(String)
     */
    public String getCharsetEncoding() {
        return charsetEncoding;
    }

    public String getJndiLocation() {
        return jndiLocation;
    }

    /**
     * Set the location where a {@link javax.mail.Session} resource is located in JNDI. Default value is
     * "java:comp/env/mail/Session".
     * 
     * @param jndiLocation
     * @since 1.0.6
     */
    public void setJndiLocation(String jndiLocation) {
        this.jndiLocation = jndiLocation;
    }

    public boolean isSessionViaJNDI() {
        return sessionViaJNDI;
    }

    /**
     * If set to true, a {@link javax.mail.Session} resource will be retrieved from JNDI. Default is false.
     * 
     * @param sessionViaJNDI
     *            whether to obtain a javax.mail.Session by JNDI
     * @since 1.0.6
     */
    public void setSessionViaJNDI(boolean sessionViaJNDI) {
        this.sessionViaJNDI = sessionViaJNDI;
    }

    /**
     * Set the character set encoding of the outgoing email messages. The default encoding is "UTF-8" which usually
     * works well for most purposes.
     * 
     * @param charsetEncoding
     */
    public void setCharsetEncoding(String charsetEncoding) {
        this.charsetEncoding = charsetEncoding;
    }

    public Layout<E> getLayout() {
        return layout;
    }

    public void setLayout(Layout<E> layout) {
        this.layout = layout;
    }

    class SenderRunnable implements Runnable {
        final CyclicBuffer<E> cyclicBuffer;
        final E e;

        SenderRunnable(CyclicBuffer<E> cyclicBuffer, E e) {
            this.cyclicBuffer = cyclicBuffer;
            this.e = e;
        }

        public void run() {
            sendBuffer(cyclicBuffer, e);
        }
    }
}
