com.zimbra.cs.mailclient.MailConnection.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.cs.mailclient.MailConnection.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software Foundation,
 * version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with this program.
 * If not, see <https://www.gnu.org/licenses/>.
 * ***** END LICENSE BLOCK *****
 */
package com.zimbra.cs.mailclient;

import org.apache.commons.codec.binary.Base64;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.SocketFactory;
import javax.security.auth.login.LoginException;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.InputStream;
import java.net.Socket;
import java.net.InetSocketAddress;
import java.net.SocketException;

import com.zimbra.common.util.Log;
import com.zimbra.cs.mailclient.util.Ascii;
import com.zimbra.cs.mailclient.auth.AuthenticatorFactory;
import com.zimbra.cs.mailclient.auth.Authenticator;

/**
 * Base class for all mail protocol connection types.
 */
public abstract class MailConnection {
    protected MailConfig config;
    protected Socket socket;
    protected Authenticator authenticator;
    protected MailInputStream mailIn;
    protected MailOutputStream mailOut;
    protected State state = State.CLOSED;
    protected String greeting;

    /** Connection states */
    protected enum State {
        CLOSED, NOT_AUTHENTICATED, AUTHENTICATED, SELECTED, LOGOUT
    }

    /**
     * Creates a new <tt>MailConnection<tt> for the specified configuration.
     *
     * @param config the <tt>MailConfig</tt> for the connection
     */
    protected MailConnection(MailConfig config) {
        this.config = config;
    }

    /**
     * Opens connection to the mail server. Does nothing if the connection
     * is already open.
     *
     * @throws IOException if an I/O error occurs
     */
    public synchronized void connect() throws IOException {
        if (!isClosed())
            return;
        try {
            socket = newSocket();
            initStreams(new BufferedInputStream(socket.getInputStream()),
                    new BufferedOutputStream(socket.getOutputStream()));
            processGreeting();
            switch (config.getSecurity()) {
            case TLS:
                startTls();
                break;
            case TLS_IF_AVAILABLE:
                try {
                    startTls();
                } catch (CommandFailedException e) {
                    getLogger().debug("STARTTLS failed", e);
                }
                break;
            }
        } catch (IOException e) {
            close();
            throw e;
        }
    }

    protected void startTls() throws IOException {
        checkState(State.NOT_AUTHENTICATED);
        sendStartTls();
        SSLSocket sock = newSSLSocket(socket);
        sock.startHandshake();
        initStreams(sock.getInputStream(), sock.getOutputStream());
    }

    private void initStreams(InputStream is, OutputStream os) {
        mailIn = newMailInputStream(is);
        mailOut = newMailOutputStream(os);
    }

    /**
     * Processes greeting message from mail server.
     *
     * @throws IOException if an I/O error occurs
     */
    protected abstract void processGreeting() throws IOException;

    /**
     * Sends login information to server. This is used if SASL authentication
     * has not been specified in the mail configuration.
     *
     * @param user the login username
     * @param pass the login password
     * @throws CommandFailedException if the login command failed
     * @throws IOException if an I/O error occurs
     */
    protected abstract void sendLogin(String user, String pass) throws IOException;

    /**
     * Sends authentication information to server. This is used if SASL
     * authentication has been specified in the mail configuration for
     * the connection. The method {@link MailConfig#getMechanism()} returns
     * the SASL mechanism to use for authentication.
     *
     * @param ir if <tt>true</tt> then sends initial response
     * @throws CommandFailedException if the authentication command failed
     * @throws IOException if an I/O error occurs
     */
    protected abstract void sendAuthenticate(boolean ir) throws IOException;

    /**
     * Sends TLS start command to the server.
     *
     * @throws CommandFailedException if the start TLS command failed
     * @throws IOException if an I/O error occurs
     */
    protected abstract void sendStartTls() throws IOException;

    /**
     * Creates a new <tt>MailInputStream</tt> for the specified input stream.
     * This method should be overridden to return a <tt>MailInputStream</tt>
     * suitable for the specific protocol type.
     *
     * @param is the underlying input stream
     * @return the new <tt>MailInputStream</tt>
     */
    protected abstract MailInputStream newMailInputStream(InputStream is);

    /**
     * Creates a new <tt>MailOutputStream</tt> for the specified output stream.
     * This method should be overriden to return a <tt>MailOutputStream</tt>
     * suitable for the specific protocol type.
     *
     * @param os the underlying output stream
     * @return the new <tt>MailOutputStream</tt>
     */
    protected abstract MailOutputStream newMailOutputStream(OutputStream os);

    /**
     * Returns the {@link Log} to use for logging mail client errors.
     *
     * @return the {@link Log} for mail client errors
     */
    public final Log getLogger() {
        return config.getLogger();
    }

    /**
     * Logs out current user from server.
     *
     * @throws IOException if an I/O error occurs
     */
    public abstract void logout() throws IOException;

    /**
     * Logs in configured user using the specified password. The login user
     * is obtained by calling {@link MailConfig#getAuthenticationId()}.
     *
     * @param pass the login password
     * @throws CommandFailedException if the login command failed
     * @throws IOException if an I/O error occurs
     */
    public synchronized void login(String pass) throws IOException {
        if (pass == null)
            throw new NullPointerException("password");
        checkState(State.NOT_AUTHENTICATED);
        String user = config.getAuthenticationId();
        if (user == null) {
            throw new IllegalStateException("Authentication id missing");
        }
        sendLogin(user, pass);
        setState(State.AUTHENTICATED);
    }

    /**
     * Authenticates the user with the specified optional password. The
     * SASL authentication method to use is obtained by calling
     * {@link MailConfig#getMechanism()}. Various other configuration
     * properties are used to support authentication.
     *
     * @param pass the authentication password, or <tt>null</tt> if not required
     * @throws CommandFailedException if the authentication command failed
     * @throws LoginException if the login failed due to a SASL authenticator error
     * @throws IOException if an I/O error occurs
     */
    public synchronized void authenticate(String pass) throws LoginException, IOException {
        String mech = config.getMechanism();
        if (mech == null || mech.equalsIgnoreCase("LOGIN")) {
            login(pass);
        } else {
            authenticate(newAuthenticator(pass));
        }
    }

    public synchronized void authenticate(Authenticator auth) throws IOException {
        authenticator = auth;
        checkState(State.NOT_AUTHENTICATED);
        sendAuthenticate(false);
        if (authenticator.isEncryptionEnabled()) {
            initStreams(authenticator.unwrap(socket.getInputStream()),
                    authenticator.wrap(socket.getOutputStream()));
        }
        setState(State.AUTHENTICATED);
    }

    private Authenticator newAuthenticator(String pass) throws LoginException, SaslException {
        AuthenticatorFactory af = config.getAuthenticatorFactory();
        if (af == null) {
            af = AuthenticatorFactory.getDefault();
        }
        return af.newAuthenticator(config, pass);
    }

    /**
     * Returns the greeting message returned by the server.
     *
     * @return the server greeting, or <tt>null</tt> if unknown
     */
    public String getGreeting() {
        return greeting;
    }

    /**
     * Processes an authentication continuation request from the server.
     * In response, this may write another continuation response to the
     * server.
     *
     * @param s the continuation request to be processed
     * @throws IOException if an I/O error occurs
     */
    protected void processContinuation(String s) throws IOException {
        byte[] decoded = Base64.decodeBase64(Ascii.getBytes(s));
        byte[] request = authenticator.evaluateChallenge(decoded);
        String data = Ascii.toString(Base64.encodeBase64(request));
        mailOut.writeLine(data);
        mailOut.flush();
    }

    /**
     * If SASL authentication was used, then returns the negotiated quality
     * of protection for the connection.
     *
     * @return the SASL quality of protection, or <tt>null</tt> if not yet
     *         authenticated or SASL authentication was not used
     */
    public String getNegotiatedQop() {
        return authenticator != null ? authenticator.getNegotiatedProperty(Sasl.QOP) : null;
    }

    /**
     * Returns the input stream for reading mail data.
     *
     * @return the connection input stream
     */
    public MailInputStream getInputStream() {
        return mailIn;
    }

    /**
     * Returns the output stream for writing mail data.
     *
     * @return the connection output stream
     */
    public MailOutputStream getOutputStream() {
        return mailOut;
    }

    /**
     * Returns the configuration for the connection.
     *
     * @return the mail configuration
     */
    public MailConfig getConfig() {
        return config;
    }

    /**
     * Sets the read timeout for the connection.
     *
     * @param readTimeout  The new read timeout, in seconds.
     *                     <tt>0</tt> means no timeout.
     * @throws SocketException if a socket I/O error occurs
     */
    public void setReadTimeout(int readTimeout) throws SocketException {
        int timeout = (int) Math.min(readTimeout * 1000L, Integer.MAX_VALUE);
        if (socket != null && !isClosed())
            socket.setSoTimeout(timeout > 0 ? timeout : Integer.MAX_VALUE);
    }

    /**
     * Returns <tt>true</tt> if the connection is closed.
     *
     * @return <tt>true</tt> if connection closed, <tt>false</tt> if not
     */
    public synchronized boolean isClosed() {
        return state == State.CLOSED;
    }

    /**
     * Returns <tt>true</tt> if the connection has been authenticated.
     *
     * @return <tt>true</tt> if connection authenticated, <tt>false<tt> if not
     */
    public synchronized boolean isAuthenticated() {
        return state == State.AUTHENTICATED;
    }

    /**
     * Returns <tt>true</tt> if connection logout is in progress.
     * @return <tt>true</tt> if logout in progress, <tt>false</tt> if not
     */
    public synchronized boolean isLogout() {
        return state == State.LOGOUT;
    }

    /**
     * @return <tt>true</tt> if in SELECTED state
     */
    public synchronized boolean isSelected() {
        return state == State.SELECTED;
    }

    /**
     * Sets the new connection state.
     *
     * @param state the new connection <tt>State</tt>
     */
    protected synchronized void setState(State state) {
        if (this.state != state) {
            getLogger().debug("setState: " + this.state + " -> " + state);
            this.state = state;
        }
    }

    /**
     * Compares current connection state with expected state. If the states
     * are not the same then throws <tt>IllegalStateException</tt>.
     *
     * @param expected the <tt>State</tt> that is expected
     * @throws IllegalStateException if the current and expected states differ
     */
    protected void checkState(State expected) {
        if (state != expected) {
            throw new IllegalStateException("Operation not supported in " + state + " state");
        }
    }

    /**
     * Closes the current connection and cleans up any associated resources.
     * The connections state is set to <tt>CLOSED</tt>.
     */
    public synchronized void close() {
        if (isClosed())
            return;
        setState(State.CLOSED);
        try {
            mailIn.close();
        } catch (IOException e) {
            // Ignore
        }
        try {
            mailOut.close();
        } catch (IOException e) {
            // Ignore
        }
        try {
            socket.close();
        } catch (IOException e) {
            getLogger().info("Error while closing connection", e);
        }
        if (authenticator != null) {
            try {
                authenticator.dispose();
            } catch (SaslException e) {
                getLogger().debug("Error while deleting authenticator", e);
            }
        }
    }

    private Socket newSocket() throws IOException {
        SocketFactory sf = config.getSecurity() != MailConfig.Security.SSL ? getSocketFactory()
                : getSSLSocketFactory();
        Socket sock = sf.createSocket();
        int connectTimeout = (int) Math.min(config.getConnectTimeout() * 1000L, Integer.MAX_VALUE);
        int readTimeout = (int) Math.min(config.getReadTimeout() * 1000L, Integer.MAX_VALUE);
        sock.setSoTimeout(readTimeout > 0 ? readTimeout : Integer.MAX_VALUE);
        sock.connect(new InetSocketAddress(config.getHost(), config.getPort()),
                connectTimeout > 0 ? connectTimeout : Integer.MAX_VALUE);
        return sock;
    }

    private SSLSocket newSSLSocket(Socket sock) throws IOException {
        return (SSLSocket) getSSLSocketFactory().createSocket(sock, sock.getInetAddress().getHostName(),
                sock.getPort(), false);
    }

    private SocketFactory getSocketFactory() {
        SocketFactory sf = config.getSocketFactory();
        return sf != null ? sf : SocketFactory.getDefault();
    }

    private SSLSocketFactory getSSLSocketFactory() {
        SSLSocketFactory ssf = config.getSSLSocketFactory();
        return ssf != null ? ssf : (SSLSocketFactory) SSLSocketFactory.getDefault();
    }

    @Override
    public String toString() {
        return String.format("{host=%s,port=%d,type=%s,state=%s}", config.getHost(), config.getPort(),
                config.getSecurity(), state);
    }
}