/*
 *  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.
 *
 */

/*
 * The original version of this class was donated by Jason Hunter,
 * who wrote the class as part of the com.oreilly.servlet
 * package for his book "Java Servlet Programming" (O'Reilly).
 * See http://www.servlets.com.
 *
 */

package org.apache.tools.mail;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Vector;
import java.util.stream.Collectors;

/**
 * A class to help send SMTP email.
 * This class is an improvement on the sun.net.smtp.SmtpClient class
 * found in the JDK.  This version has extra functionality, and can be used
 * with JVMs that did not extend from the JDK.  It's not as robust as
 * the JavaMail Standard Extension classes, but it's easier to use and
 * easier to install, and has an Open Source license.
 * <p>
 * It can be used like this:
 * </p>
 * <pre>
 * String mailhost = "localhost";  // or another mail host
 * String from = "Mail Message Servlet &lt;MailMessage@server.com&gt;";
 * String to = "to@you.com";
 * String cc1 = "cc1@you.com";
 * String cc2 = "cc2@you.com";
 * String bcc = "bcc@you.com";
 *
 * MailMessage msg = new MailMessage(mailhost);
 * msg.setPort(25);
 * msg.from(from);
 * msg.to(to);
 * msg.cc(cc1);
 * msg.cc(cc2);
 * msg.bcc(bcc);
 * msg.setSubject("Test subject");
 * PrintStream out = msg.getPrintStream();
 *
 * Enumeration enum = req.getParameterNames();
 * while (enum.hasMoreElements()) {
 *   String name = (String) enum.nextElement();
 *   String value = req.getParameter(name);
 *   out.println(name + " = " + value);
 * }
 *
 * msg.sendAndClose();
 * </pre>
 * <p>
 * Be sure to set the from address, then set the recipient
 * addresses, then set the subject and other headers, then get the
 * PrintStream, then write the message, and finally send and close.
 * The class does minimal error checking internally; it counts on the mail
 * host to complain if there's any malformatted input or out of order
 * execution.
 * </p>
 * <p>
 * An attachment mechanism based on RFC 1521 could be implemented on top of
 * this class.  In the meanwhile, JavaMail is the best solution for sending
 * email with attachments.
 * </p>
 * <p>
 * Still to do:
 * </p>
 * <ul>
 * <li>Figure out how to close the connection in case of error</li>
 * </ul>
 *
 * @version 1.1, 2000/03/19, added angle brackets to address, helps some servers
 * version 1.0, 1999/12/29
 */
public class MailMessage {

    /** default mailhost */
    public static final String DEFAULT_HOST = "localhost";

    /** default port for SMTP: 25 */
    public static final int DEFAULT_PORT = 25;

    /** host name for the mail server */
    private String host;

    /** host port for the mail server */
    private int port = DEFAULT_PORT;

    /** sender email address */
    private String from;

    /** list of email addresses to reply to */
    private final Vector<String> replyto = new Vector<>();

    /** list of email addresses to send to */
    private final Vector<String> to = new Vector<>();

    /** list of email addresses to cc to */
    private final Vector<String> cc = new Vector<>();

    /** headers to send in the mail */
    private final Map<String, String> headers = new LinkedHashMap<>();

    private MailPrintStream out;

    private SmtpResponseReader in;

    private Socket socket;

    private static final int OK_READY = 220;
    private static final int OK_HELO = 250;
    private static final int OK_FROM = 250;
    private static final int OK_RCPT_1 = 250;
    private static final int OK_RCPT_2 = 251;
    private static final int OK_DATA = 354;
    private static final int OK_DOT = 250;
    private static final int OK_QUIT = 221;

    /**
     * Constructs a new MailMessage to send an email.
     * Use localhost as the mail server with port 25.
     *
     * @exception IOException if there's any problem contacting the mail server
     */
    public MailMessage() throws IOException {
        this(DEFAULT_HOST, DEFAULT_PORT);
    }

    /**
     * Constructs a new MailMessage to send an email.
     * Use the given host as the mail server with port 25.
     *
     * @param host the mail server to use
     * @exception IOException if there's any problem contacting the mail server
     */
    public MailMessage(String host) throws IOException {
        this(host, DEFAULT_PORT);
    }

    /**
     * Constructs a new MailMessage to send an email.
     * Use the given host and port as the mail server.
     *
     * @param host the mail server to use
     * @param port the port to connect to
     * @exception IOException if there's any problem contacting the mail server
     */
    public MailMessage(String host, int port) throws IOException {
        this.port = port;
        this.host = host;
        connect();
        sendHelo();
    }

    /**
     * Set the port to connect to the SMTP host.
     * @param port the port to use for connection.
     * @see #DEFAULT_PORT
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * Sets the from address.  Also sets the "From" header.  This method should
     * be called only once.
     * @param from the from address
     * @exception IOException if there's any problem reported by the mail server
     */
    public void from(String from) throws IOException {
        sendFrom(from);
        this.from = from;
    }

    /**
     * Sets the replyto address
     * This method may be
     * called multiple times.
     * @param rto the replyto address
     *
     */
    public void replyto(String rto) {
        this.replyto.addElement(rto);
    }

    /**
     * Sets the to address.  Also sets the "To" header.  This method may be
     * called multiple times.
     *
     * @param to the to address
     * @exception IOException if there's any problem reported by the mail server
     */
    public void to(String to) throws IOException {
        sendRcpt(to);
        this.to.addElement(to);
    }

    /**
     * Sets the cc address.  Also sets the "Cc" header.  This method may be
     * called multiple times.
     *
     * @param cc the cc address
     * @exception IOException if there's any problem reported by the mail server
     */
    public void cc(String cc) throws IOException {
        sendRcpt(cc);
        this.cc.addElement(cc);
    }

    /**
     * Sets the bcc address.  Does NOT set any header since it's a *blind* copy.
     * This method may be called multiple times.
     *
     * @param bcc the bcc address
     * @exception IOException if there's any problem reported by the mail server
     */
    public void bcc(String bcc) throws IOException {
        sendRcpt(bcc);
        // No need to keep track of Bcc'd addresses
    }

    /**
     * Sets the subject of the mail message.  Actually sets the "Subject"
     * header.
     * @param subj the subject of the mail message
     */
    public void setSubject(String subj) {
        setHeader("Subject", subj);
    }

    /**
     * Sets the named header to the given value.  RFC 822 provides the rules for
     * what text may constitute a header name and value.
     * @param name name of the header
     * @param value contents of the header
     */
    public void setHeader(String name, String value) {
        // Blindly trust the user doesn't set any invalid headers
        headers.put(name, value);
    }

    /**
     * Returns a PrintStream that can be used to write the body of the message.
     * A stream is used since email bodies are byte-oriented.  A writer can
     * be wrapped on top if necessary for internationalization.
     * This is actually done in Message.java
     *
     * @return a printstream containing the data and the headers of the email
     * @exception IOException if there's any problem reported by the mail server
     * @see org.apache.tools.ant.taskdefs.email.Message
     */
    public PrintStream getPrintStream() throws IOException {
        setFromHeader();
        setReplyToHeader();
        setToHeader();
        setCcHeader();
        setHeader("X-Mailer",
            "org.apache.tools.mail.MailMessage (ant.apache.org)");
        sendData();
        flushHeaders();
        return out;
    }

    // RFC 822 s4.1: "From:" header must be sent
    // We rely on error checking by the MTA
    void setFromHeader() {
        setHeader("From", from);
    }

    // RFC 822 s4.1: "Reply-To:" header is optional
    void setReplyToHeader() {
        if (!replyto.isEmpty()) {
            setHeader("Reply-To", vectorToList(replyto));
        }
    }

    void setToHeader() {
        if (!to.isEmpty()) {
            setHeader("To", vectorToList(to));
        }
    }

    void setCcHeader() {
        if (!cc.isEmpty()) {
            setHeader("Cc", vectorToList(cc));
        }
    }

    String vectorToList(Vector<String> v) {
        return v.stream().collect(Collectors.joining(", "));
    }

    void flushHeaders() throws IOException {
        // RFC 822 s4.1:
        //   "Header fields are NOT required to occur in any particular order,
        //    except that the message body MUST occur AFTER the headers"
        // (the same section specifies a recommended order, which we ignore)
        headers.forEach((k, v) -> out.printf("%s: %s%n", k, v));
        out.println();
        out.flush();
    }

    /**
     * Sends the message and closes the connection to the server.
     * The MailMessage object cannot be reused.
     *
     * @exception IOException if there's any problem reported by the mail server
     */
    public void sendAndClose() throws IOException {
        try {
            sendDot();
            sendQuit();
        } finally {
            disconnect();
        }
    }

    // Make a limited attempt to extract a sanitized email address
    // Prefer text in <brackets>, ignore anything in (parentheses)
    static String sanitizeAddress(String s) {
        int paramDepth = 0;
        int start = 0;
        int end = 0;
        int len = s.length();

        for (int i = 0; i < len; i++) {
            char c = s.charAt(i);
            if (c == '(') {
                paramDepth++;
                if (start == 0) {
                    end = i; // support "address (name)"
                }
            } else if (c == ')') {
                paramDepth--;
                if (end == 0) {
                    start = i + 1; // support "(name) address"
                }
            } else if (paramDepth == 0 && c == '<') {
                start = i + 1;
            } else if (paramDepth == 0 && c == '>') {
                end = i;
            }
        }

        if (end == 0) {
            end = len;
        }

        return s.substring(start, end);
    }

    // * * * * * Raw protocol methods below here * * * * *

    void connect() throws IOException {
        socket = new Socket(host, port);
        out = new MailPrintStream(
            new BufferedOutputStream(socket.getOutputStream()));
        in = new SmtpResponseReader(socket.getInputStream());
        getReady();
    }

    void getReady() throws IOException {
        String response = in.getResponse();
        int[] ok = {OK_READY};
        if (!isResponseOK(response, ok)) {
            throw new IOException(
                "Didn't get introduction from server: " + response);
        }
    }

    void sendHelo() throws IOException {
        String local = InetAddress.getLocalHost().getHostName();
        int[] ok = {OK_HELO};
        send("HELO " + local, ok);
    }

    void sendFrom(String from) throws IOException {
        int[] ok = {OK_FROM};
        send("MAIL FROM: " + "<" + sanitizeAddress(from) + ">", ok);
    }

    void sendRcpt(String rcpt) throws IOException {
        int[] ok = {OK_RCPT_1, OK_RCPT_2};
        send("RCPT TO: " + "<" + sanitizeAddress(rcpt) + ">", ok);
    }

    void sendData() throws IOException {
        int[] ok = {OK_DATA};
        send("DATA", ok);
    }

    void sendDot() throws IOException {
        int[] ok = {OK_DOT};
        send("\r\n.", ok); // make sure dot is on new line
    }

    void sendQuit() throws IOException {
        int[] ok = {OK_QUIT};
        try {
            send("QUIT", ok);
        } catch (IOException e) {
            throw new ErrorInQuitException(e);
        }
    }

    void send(String msg, int[] ok) throws IOException {
        out.rawPrint(msg + "\r\n"); // raw supports <CRLF>.<CRLF>
        String response = in.getResponse();
        if (!isResponseOK(response, ok)) {
            throw new IOException(
                "Unexpected reply to command: " + msg + ": " + response);
        }
    }

    boolean isResponseOK(String response, int[] ok) {
        // Check that the response is one of the valid codes
        for (int i = 0; i < ok.length; i++) {
            if (response.startsWith("" + ok[i])) {
                return true;
            }
        }
        return false;
    }

    void disconnect() throws IOException {
        if (out != null) {
            out.close();
        }
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                // ignore
            }
        }
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }
}

/**
 * This PrintStream subclass makes sure that <CRLF>. becomes <CRLF>..
 *  per RFC 821.  It also ensures that new lines are always \r\n.
*/
class MailPrintStream extends PrintStream {

    private int lastChar;

    public MailPrintStream(OutputStream out) {
        super(out, true); // deprecated, but email is byte-oriented
    }

    // Mac does \n\r, but that's tough to distinguish from Windows \r\n\r\n.
    // Don't tackle that problem right now.
    @Override
    public void write(int b) {
        if (b == '\n' && lastChar != '\r') {
            rawWrite('\r'); // ensure always \r\n
            rawWrite(b);
        } else if (b == '.' && lastChar == '\n') {
            rawWrite('.'); // add extra dot
            rawWrite(b);
        } else {
            rawWrite(b);
        }
        lastChar = b;
    }

    @Override
    public void write(byte[] buf, int off, int len) {
        for (int i = 0; i < len; i++) {
            write(buf[off + i]);
        }
    }

    void rawWrite(int b) {
        super.write(b);
    }

    void rawPrint(String s) {
        int len = s.length();
        for (int i = 0; i < len; i++) {
            rawWrite(s.charAt(i));
        }
    }
}
