com.zimbra.cs.datasource.imap.ConnectionManager.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.cs.datasource.imap.ConnectionManager.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015, 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.datasource.imap;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.security.auth.login.LoginException;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.json.JSONObject;

import com.zimbra.common.account.ZAttrProvisioning.DataSourceAuthMechanism;
import com.zimbra.common.localconfig.LC;
import com.zimbra.common.net.SocketFactories;
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.util.Log;
import com.zimbra.common.util.ZimbraHttpConnectionManager;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.DataSource;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.datasource.DataSourceManager;
import com.zimbra.cs.datasource.MessageContent;
import com.zimbra.cs.datasource.SyncUtil;
import com.zimbra.cs.mailclient.CommandFailedException;
import com.zimbra.cs.mailclient.MailConfig;
import com.zimbra.cs.mailclient.MailConfig.Security;
import com.zimbra.cs.mailclient.auth.Authenticator;
import com.zimbra.cs.mailclient.auth.AuthenticatorFactory;
import com.zimbra.cs.mailclient.auth.SaslAuthenticator;
import com.zimbra.cs.mailclient.imap.CAtom;
import com.zimbra.cs.mailclient.imap.DataHandler;
import com.zimbra.cs.mailclient.imap.IDInfo;
import com.zimbra.cs.mailclient.imap.ImapCapabilities;
import com.zimbra.cs.mailclient.imap.ImapConfig;
import com.zimbra.cs.mailclient.imap.ImapConnection;
import com.zimbra.cs.mailclient.imap.ImapData;
import com.zimbra.cs.mailclient.imap.ImapResponse;
import com.zimbra.cs.mailclient.imap.ResponseHandler;
import com.zimbra.cs.util.BuildInfo;
import com.zimbra.cs.util.JMSession;
import com.zimbra.cs.util.Zimbra;
import com.zimbra.soap.type.DataSource.ConnectionType;

final class ConnectionManager {
    private Map<String, ImapConnection> connections = Collections
            .synchronizedMap(new HashMap<String, ImapConnection>());

    private static final ConnectionManager INSTANCE = new ConnectionManager();

    private static final boolean REUSE_CONNECTIONS = LC.data_source_imap_reuse_connections.booleanValue();

    private static final int IDLE_READ_TIMEOUT = 30 * 60; // 30 minutes

    private static final Log LOG = ZimbraLog.datasource;

    public static ConnectionManager getInstance() {
        return INSTANCE;
    }

    private ConnectionManager() {
    }

    /**
     * Opens a new IMAP connection or reuses an existing one if available
     * for specified data source. If a new connection is required the optional
     * authenticator, if specified, will be used with AUTHENTICATE, otherwise
     * the LOGIN command will be used. If a suspended connection is reused
     * and has been idling, then IDLE will be automatically terminated.
     *
     * @param ds the data source for the connection
     * @param auth optional authenticator, or null to use LOGIN
     * @return the IMAP connection to use
     * @throws ServiceException if an I/O or auth error occurred
     */
    public ImapConnection openConnection(DataSource ds, Authenticator auth) throws ServiceException {
        ImapConnection ic = connections.remove(ds.getId());
        if (ic == null || !resumeConnection(ic)) {
            ic = newConnection(ds, auth);
        }
        ic.getImapConfig().setMaxLiteralMemSize(ds.getMaxTraceSize());
        return ic;
    }

    public ImapConnection openConnection(DataSource ds) throws ServiceException {
        return openConnection(ds, null);
    }

    /**
     * Releases existing connection so that it can be reused again. If the
     * IMAP server supports IDLE then the connection will go into IDLE state.
     * Otherwise, the connection will be kept active for as long as possible.
     *
     * @param ds the data source for the connection
     * @param ic the connection to release
     */
    public void releaseConnection(DataSource ds, ImapConnection ic) {
        LOG.debug("Releasing connection: " + ic);
        if (isReuseConnections(ds) && suspendConnection(ds, ic)) {
            ImapConnection conn = connections.put(ds.getId(), ic);
            if (conn != null) {
                LOG.debug("More than one suspended connection for: %s. closing the oldest: %s", ds, conn);
                conn.close(); //there was another suspended connection; just close it
            }
        } else {
            LOG.debug("Closing connection: " + ic);
            ic.close();
        }
    }

    public void closeConnection(DataSource ds) {
        closeConnection(ds.getId());
    }

    /**
     * Closes any suspended connection associated with specified data source.
     * This must be called whenever data source is modified or deleted in order
     * to force a reconnect upon next use.
     *
     * @param dataSourceId the data source id for the connection
     */
    public void closeConnection(String dataSourceId) {
        ImapConnection ic = connections.remove(dataSourceId);
        if (ic != null) {
            LOG.debug("Closing connection: " + ic);
            ic.close();
        }
    }

    private boolean isReuseConnections(DataSource ds) {
        return ds.isOffline() && REUSE_CONNECTIONS;
    }

    public static ImapConnection newConnection(DataSource ds, Authenticator auth) throws ServiceException {
        ImapConfig config = newImapConfig(ds);
        ImapConnection ic = new ImapConnection(config);
        ic.setDataHandler(new FetchDataHandler());
        try {
            ic.connect();
            try {
                if (config.getMechanism() != null) {
                    if (SaslAuthenticator.XOAUTH2.equalsIgnoreCase(config.getMechanism())) {
                        auth = AuthenticatorFactory.getDefault().newAuthenticator(config,
                                ds.getDecryptedOAuthToken());
                    } else {
                        auth = AuthenticatorFactory.getDefault().newAuthenticator(config,
                                ds.getDecryptedPassword());
                    }
                }
                if (auth == null) {
                    ic.login(ds.getDecryptedPassword());
                } else {
                    ic.authenticate(auth);
                }
            } catch (CommandFailedException e) {
                if (SaslAuthenticator.XOAUTH2.equalsIgnoreCase(config.getMechanism())) {
                    try {
                        DataSourceManager.refreshOAuthToken(ds);
                        config.getSaslProperties().put(
                                "mail." + config.getProtocol() + ".sasl.mechanisms.oauth2.oauthToken",
                                ds.getDecryptedOAuthToken());
                        auth = AuthenticatorFactory.getDefault().newAuthenticator(config,
                                ds.getDecryptedOAuthToken());
                        ic.authenticate(auth);
                    } catch (CommandFailedException e1) {
                        ZimbraLog.datasource.warn("Exception in connecting to data source", e);
                        throw new LoginException(e1.getError());
                    }
                } else {
                    throw new LoginException(e.getError());
                }
            }
            if (isImportingSelf(ds, ic)) {
                throw ServiceException.INVALID_REQUEST("User attempted to import messages from his/her own mailbox",
                        null);
            }
        } catch (ServiceException e) {
            ic.close();
            throw e;
        } catch (Exception e) {
            ic.close();
            throw ServiceException.FAILURE("Unable to connect to IMAP server: " + ds, e);
        }
        LOG.debug("Created new connection: " + ic);
        return ic;
    }

    private static boolean isImportingSelf(DataSource ds, ImapConnection ic) throws IOException, ServiceException {
        if (!ds.isOffline() && ic.hasCapability(ImapCapabilities.ID)) {
            try {
                IDInfo clientId = new IDInfo();
                clientId.put(IDInfo.NAME, IDInfo.DATASOURCE_IMAP_CLIENT_NAME);
                clientId.put(IDInfo.VERSION, BuildInfo.VERSION);
                IDInfo id = ic.id(clientId);
                if ("Zimbra".equalsIgnoreCase(id.get(IDInfo.NAME)) && ds.getAccount() != null) {
                    String user = id.get("user");
                    String server = id.get("server");
                    return user != null && user.equals(ds.getAccount().getName()) && server != null
                            && server.equals(Provisioning.getInstance().getLocalServer().getId());
                }
            } catch (CommandFailedException e) {
                // Skip check if ID command fails
                LOG.warn("ID command failed, assuming not importing self", e);
            }
        }
        return false;
    }

    // Handler for fetched message data which uses ParsedMessage to stream
    // the message data to disk if necessary.
    private static class FetchDataHandler implements DataHandler {
        @Override
        public Object handleData(ImapData data) throws Exception {
            try {
                return MessageContent.read(data.getInputStream(), data.getSize());
            } catch (OutOfMemoryError e) {
                Zimbra.halt("Out of memory", e);
                return null;
            }
        }
    }

    public static ImapConfig newImapConfig(DataSource ds) {
        ImapConfig config = new ImapConfig();
        Map<String, String> props = new HashMap<String, String>();
        config.setHost(ds.getHost());
        config.setPort(ds.getPort());
        config.setAuthenticationId(ds.getUsername());
        config.setSecurity(getSecurity(ds));
        config.setMechanism(ds.getAuthMechanism());
        config.setAuthorizationId(ds.getAuthId());
        if (SaslAuthenticator.XOAUTH2.equalsIgnoreCase(ds.getAuthMechanism())) {
            try {
                JMSession.addOAuth2Properties(ds.getDecryptedOAuthToken(), props, config.getProtocol());
                config.setSaslProperties(props);
            } catch (ServiceException e) {
                ZimbraLog.datasource.warn("Exception in decrypting the oauth token", e);
            }
        }
        // bug 37982: Disable use of LITERAL+ due to problems with Yahoo IMAP.
        // Avoiding LITERAL+ also gives servers a chance to reject uploaded
        // messages that are too big, since the server must send a continuation
        // response before the literal data can be sent.
        config.setUseLiteralPlus(false);
        // Enable support for trace output
        if (ds.isDebugTraceEnabled()) {
            config.setLogger(SyncUtil.getTraceLogger(ZimbraLog.imap_client, ds.getId()));
        }
        config.setSocketFactory(SocketFactories.defaultSocketFactory());
        config.setSSLSocketFactory(SocketFactories.defaultSSLSocketFactory());
        config.setConnectTimeout(ds.getConnectTimeout(LC.javamail_imap_timeout.intValue()));
        config.setReadTimeout(ds.getReadTimeout(LC.javamail_imap_timeout.intValue()));
        LOG.debug("Connect timeout = %d, read timeout = %d", config.getConnectTimeout(), config.getReadTimeout());
        return config;
    }

    private static MailConfig.Security getSecurity(DataSource ds) {
        ConnectionType type = ds.getConnectionType();
        if (type == null) {
            type = ConnectionType.cleartext;
        }
        switch (type) {
        case cleartext:
            // bug 44439: For ZCS import, if connection type is 'cleartext' we
            // still use localconfig property to determine if we should try
            // TLS. This maintains compatibility with 5.0.x since there is
            // still no UI setting to explicitly enable TLS. For desktop
            // this forced a plaintext connection since we have the UI options.
            return !ds.isOffline() && LC.javamail_imap_enable_starttls.booleanValue() ? Security.TLS_IF_AVAILABLE
                    : Security.NONE;
        case ssl:
            return Security.SSL;
        case tls:
            return Security.TLS;
        case tls_if_available:
            return Security.TLS_IF_AVAILABLE;
        default:
            return Security.NONE;
        }
    }

    private static boolean suspendConnection(DataSource ds, ImapConnection ic) {
        // If IDLE supported then IDLE connection, otherwise just let it sit
        if (ic.isClosed()) {
            return false;
        }
        try {
            if (ic.hasIdle()) {
                ic.setReadTimeout(IDLE_READ_TIMEOUT);
                if (!ic.isSelected("INBOX")) {
                    ic.select("INBOX");
                }
                ic.idle(idleHandler(ds));
            } else if (ic.isSelected()) {
                if (ic.hasUnselect()) {
                    ic.unselect();
                } else {
                    ic.close_mailbox();
                }
            }
            LOG.debug("Suspended connection: " + ic);
        } catch (IOException e) {
            LOG.warn("Error suspending connection", e);
        }
        return true;
    }

    private static ResponseHandler idleHandler(final DataSource ds) {
        return new ResponseHandler() {
            @Override
            public void handleResponse(ImapResponse res) throws Exception {
                if (res.getCCode() == CAtom.EXISTS) {
                    SyncState ss = SyncStateManager.getInstance().getOrCreateSyncState(ds);
                    if (ss != null) {
                        ss.setHasRemoteInboxChanges(true);
                    }
                }
            }
        };
    }

    private static boolean resumeConnection(ImapConnection ic) {
        if (ic.isClosed()) {
            return false;
        }
        try {
            ic.setReadTimeout(ic.getImapConfig().getReadTimeout());
            if (ic.isIdling()) {
                if (!ic.stopIdle()) {
                    return false;
                }
            } else {
                ic.noop();
            }
            LOG.debug("Resumed connection: " + ic);
        } catch (IOException e) {
            LOG.warn("Error resuming connection: " + ic, e);
            return false;
        }
        return true;
    }

}