All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.subethamail.smtp.client.SMTPClient Maven / Gradle / Ivy

Go to download

A fork of a fork (!) of SubEtha, an easy-to-use server-side SMTP library for Java.

There is a newer version: 7.1.3
Show newest version
package org.subethamail.smtp.client;

import java.io.BufferedReader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subethamail.smtp.internal.Constants;
import org.subethamail.smtp.internal.io.DotTerminatedOutputStream;
import org.subethamail.smtp.internal.io.ExtraDotOutputStream;

import com.github.davidmoten.guavamini.Preconditions;

/**
 * A very low level abstraction of the STMP stream which knows how to handle the
 * raw protocol for lines, whitespace, etc.
 *
 * @author Jeff Schnitzer
 */
public final class SMTPClient {
    /** 5 minutes */
    private static final int CONNECT_TIMEOUT = 300 * 1000;

    /** 10 minutes */
    private static final int REPLY_TIMEOUT = 600 * 1000;

    private static Logger log = LoggerFactory.getLogger(SMTPClient.class);

    /** the local socket address */
    private final Optional bindpoint;

    private volatile SocketAddress localSocketAddress;

    /**
     * True if the client has been successfully connected to the server and not
     * it has not been closed yet.
     **/
    private boolean connected;

    /** Just for display purposes */
    private final Optional hostPortName;

    /** The raw socket */
    Socket socket;

    BufferedReader reader;

    /** Output streams used for data */
    OutputStream rawOutput;
    /**
     * A stream which wraps {@link #rawOutput} and is used to write out the DOT
     * CR LF terminating sequence in the DATA command, if necessary
     * complementing the message content with a closing CR LF.
     */
    DotTerminatedOutputStream dotTerminatedOutput;
    /**
     * This stream wraps {@link #dotTerminatedOutput} and it does the dot
     * stuffing for the SMTP DATA command.
     */
    ExtraDotOutputStream dataOutput;

    /** Note we bypass this during DATA */
    PrintWriter writer;

    /**
     * Result of an SMTP exchange.
     */
    public static final class Response {
        int code;
        String message;

        public Response(int code, String text) {
            this.code = code;
            this.message = text;
        }

        public int getCode() {
            return this.code;
        }

        public String getMessage() {
            return this.message;
        }

        public boolean isSuccess() {
            return this.code >= 100 && this.code < 400;
        }

        @Override
        public String toString() {
            return this.code + " " + this.message;
        }
    }

    /**
     * Establishes a connection to host and port.
     * 
     * @throws UnknownHostException
     *             if the hostname cannot be resolved
     * @throws IOException
     *             if there is a problem connecting to the port
     */
    public SMTPClient() throws UnknownHostException, IOException {
        this(Optional.empty(), Optional.empty());
    }

    /**
     * Constructor.
     * 
     * @param bindpoint
     *            the local socket address. If empty, the system will pick up an
     *            ephemeral port and a valid local address.
     * @param hostPortName
     *            Sets the name of the remote MTA for informative purposes.
     *            Default is host:port, where host and port are the values which
     *            were used to open the TCP connection to the server, as they
     *            were passed to the connect method.
     * @throws UnknownHostException
     *             if the hostname cannot be resolved
     * @throws IOException
     *             if there is a problem connecting to the port
     */
    public SMTPClient(Optional bindpoint, Optional hostPortName)
            throws UnknownHostException, IOException {
        Preconditions.checkNotNull(bindpoint, "bindpoint cannot be null");
        this.bindpoint = bindpoint;
        this.hostPortName = hostPortName;
    }

    public static SMTPClient createAndConnect(String host, int port)
            throws UnknownHostException, IOException {
        return createAndConnect(host, port, Optional.empty());
    }

    public static SMTPClient createAndConnect(String host, int port, Optional hostPortName)
            throws UnknownHostException, IOException {
        SMTPClient client = new SMTPClient(Optional.empty(),
                Optional.of(hostPortName.orElse(host + ":" + port)));
        client.connect(host, port);
        return client;
    }

    /**
     * Establishes a connection to host and port.
     * 
     * @throws IOException
     *             if there is a problem connecting to the port
     */
    public void connect(String host, int port) throws IOException {
        if (connected)
            throw new IllegalStateException("Already connected");

        log.debug("Connecting to {}", this.hostPortName);

        this.socket = createSocket();
        this.socket.bind(this.bindpoint.orElse(null));
        this.socket.setSoTimeout(REPLY_TIMEOUT);
        this.socket.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT);

        try {
            this.localSocketAddress = this.socket.getLocalSocketAddress();

            this.reader = new BufferedReader(
                    new InputStreamReader(this.socket.getInputStream(), Constants.SMTP_CHARSET));

            this.rawOutput = this.socket.getOutputStream();
            this.dotTerminatedOutput = new DotTerminatedOutputStream(this.rawOutput);
            this.dataOutput = new ExtraDotOutputStream(this.dotTerminatedOutput);
            this.writer = new PrintWriter(this.rawOutput, true);
        } catch (IOException e) {
            close();
            throw e;
        }

        connected = true;
    }

    /**
     * Returns a new unconnected socket.
     * 

* Implementation notice for subclasses: This function is called by the * constructors which open the connection immediately. In these cases the * subclass is not yet initialized, therefore subclasses overriding this * function shouldn't use those constructors. */ protected Socket createSocket() { return new Socket(); } /** * Returns true if the client is connected to the server. */ public boolean isConnected() { return connected; } /** * Sends a message to the server, ie "HELO foo.example.com". A newline will * be appended to the message. * * @param msg * should not have any newlines */ protected void send(String msg) throws IOException { log.debug("Client: {}", msg); if (!connected) throw new IllegalStateException("Not connected"); // Force \r\n since println() behaves differently on different platforms this.writer.print(msg + "\r\n"); this.writer.flush(); } /** * Note that the response text comes back without trailing newlines. */ protected Response receive() throws IOException { if (!connected) throw new IllegalStateException("Not connected"); StringBuilder builder = new StringBuilder(); String line = null; boolean done = false; do { line = this.reader.readLine(); if (line == null) { if (builder.length() == 0) throw new EOFException("Server disconnected unexpectedly, no reply received"); else throw new IOException("Malformed SMTP reply: " + builder); } log.debug("Server: {}", line); if (line.length() < 4) throw new IOException("Malformed SMTP reply: " + line); builder.append(line.substring(4)); if (line.charAt(3) == '-') builder.append('\n'); else done = true; } while (!done); int code; String codeString = line.substring(0, 3); try { code = Integer.parseInt(codeString); } catch (NumberFormatException e) { throw new IOException("Malformed SMTP reply: " + line, e); } return new Response(code, builder.toString()); } /** * Sends a message to the server, ie "HELO foo.example.com". A newline will * be appended to the message. * * @param msg * should not have any newlines * @return the response from the server */ public Response sendReceive(String msg) throws IOException { this.send(msg); return this.receive(); } /** If response is not success, throw an exception */ public Response receiveAndCheck() throws IOException, SMTPException { Response resp = this.receive(); if (!resp.isSuccess()) throw new SMTPException(resp); return resp; } /** If response is not success, throw an exception */ public Response sendAndCheck(String msg) throws IOException, SMTPException { this.send(msg); return this.receiveAndCheck(); } /** Logs but otherwise ignores errors */ public void close() { connected = false; if (this.socket != null && !this.socket.isClosed()) { try { this.socket.close(); log.debug("Closed connection to {}", this.hostPortName); } catch (IOException ex) { log.error("Problem closing connection to " + this.hostPortName, ex); } } } @Override public String toString() { return this.getClass().getSimpleName() + " { " + this.hostPortName + "}"; } /** * Returns the local socket address. */ public SocketAddress getLocalSocketAddress() { return localSocketAddress; } /** * @return a nice pretty description of who we are connected to */ public String getHostPort() { return this.hostPortName.orElse(null); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy