com.zimbra.cs.mailclient.imap.ImapConnection.java Source code

Java tutorial

Introduction

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

Source

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

import com.zimbra.common.util.Constants;
import com.zimbra.cs.mailclient.MailConnection;
import com.zimbra.cs.mailclient.MailException;
import com.zimbra.cs.mailclient.MailInputStream;
import com.zimbra.cs.mailclient.MailOutputStream;
import com.zimbra.cs.mailclient.CommandFailedException;
import com.zimbra.cs.mailclient.ParseException;
import com.zimbra.cs.mailclient.util.Ascii;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.util.Formatter;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

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

public final class ImapConnection extends MailConnection {
    private ImapCapabilities capabilities;
    private MailboxInfo mailbox;
    private ImapRequest request;
    private DataHandler dataHandler;
    private Character delimiter;

    private final AtomicInteger tagCount = new AtomicInteger();

    private static final String TAG_FORMAT = "C%02d";

    public ImapConnection(ImapConfig config) {
        super(config);
    }

    public void setDataHandler(DataHandler handler) {
        dataHandler = handler;
    }

    public DataHandler getDataHandler() {
        return dataHandler;
    }

    @Override
    protected MailInputStream newMailInputStream(InputStream is) {
        if (getLogger().isTraceEnabled()) {
            return new ImapInputStream(is, this, getLogger());
        } else {
            return new ImapInputStream(is, this);
        }
    }

    @Override
    protected MailOutputStream newMailOutputStream(OutputStream os) {
        if (getLogger().isTraceEnabled()) {
            return new ImapOutputStream(os, getLogger());
        } else {
            return new ImapOutputStream(os);
        }
    }

    @Override
    protected void processGreeting() throws IOException {
        ImapResponse res = readResponse();
        if (res.isUntagged()) {
            greeting = res.getResponseText().getText();
            switch (res.getCCode()) {
            case BYE:
                throw new MailException(greeting);
            case PREAUTH:
            case OK:
                setState(res.isOK() ? State.NOT_AUTHENTICATED : State.AUTHENTICATED);
                ResponseText rt = res.getResponseText();
                if (CAtom.CAPABILITY.atom().equals(rt.getCode())) {
                    capabilities = (ImapCapabilities) rt.getData();
                } else {
                    capability();
                }
                return;
            }
        }
        throw new MailException("Expected server greeting but got: " + res);
    }

    @Override
    protected void sendLogin(String user, String pass) throws IOException {
        newRequest(CAtom.LOGIN, ImapData.asAString(user), ImapData.asAString(pass)).sendCheckStatus();
    }

    @Override
    public synchronized void logout() throws IOException {
        if (isShutdown())
            return;
        if (request != null) {
            throw new IllegalStateException("Request pending");
        }
        setState(State.LOGOUT);
        try {
            newRequest(CAtom.LOGOUT).sendCheckStatus();
        } catch (CommandFailedException e) {
            getLogger().warn("Logout failed, force closing connection", e);
            close();
        }
    }

    @Override
    protected void sendAuthenticate(boolean ir) throws IOException {
        ImapRequest req = newRequest(CAtom.AUTHENTICATE, authenticator.getMechanism());
        if (ir) {
            byte[] response = authenticator.getInitialResponse();
            req.addParam(Ascii.toString(Base64.encodeBase64(response)));
        }
        req.sendCheckStatus();
    }

    @Override
    protected void sendStartTls() throws IOException {
        newRequest(CAtom.STARTTLS).sendCheckStatus();
    }

    public ImapCapabilities capability() throws IOException {
        newRequest(CAtom.CAPABILITY).sendCheckStatus();
        return capabilities;
    }

    public void noop() throws IOException {
        newRequest(CAtom.NOOP).sendCheckStatus();
    }

    public void noop(ResponseHandler handler) throws IOException {
        ImapRequest req = newRequest(CAtom.NOOP);
        req.setResponseHandler(handler);
        req.sendCheckStatus();
    }

    public void idle(ResponseHandler handler) throws IOException {
        ImapRequest req = newRequest(CAtom.IDLE);
        req.setResponseHandler(handler);
        ImapResponse res = req.send();
        if (res != null) {
            if (res.isOK()) {
                throw new MailException("Expected IDLE continuation but got final response");
            }
            req.checkStatus(res);
        }
    }

    public void check() throws IOException {
        newRequest(CAtom.CHECK).sendCheckStatus();
    }

    public void xatom(String cmd, Object... params) throws IOException {
        newRequest(cmd, params).sendCheckStatus();
    }

    public IDInfo id(IDInfo info) throws IOException {
        ImapRequest req = newRequest(CAtom.ID, info == null ? CAtom.NIL : info);
        List<IDInfo> results = new ArrayList<IDInfo>(1);
        req.setResponseHandler(new BasicResponseHandler(CAtom.ID, results));
        req.sendCheckStatus();
        return results.isEmpty() ? new IDInfo() : results.get(0);
    }

    public synchronized boolean isSelected(String name) {
        return mailbox != null && mailbox.getName().equals(name);
    }

    public synchronized MailboxInfo select(String name) throws IOException {
        mailbox = doSelectOrExamine(CAtom.SELECT, name);
        setState(State.SELECTED);
        return getMailboxInfo();
    }

    public MailboxInfo examine(String name) throws IOException {
        return doSelectOrExamine(CAtom.EXAMINE, name);
    }

    private MailboxInfo doSelectOrExamine(CAtom cmd, String name) throws IOException {
        MailboxInfo mbox = new MailboxInfo(name);
        ImapRequest req = newRequest(cmd, new MailboxName(name));
        req.setResponseHandler(mbox);
        mbox.handleResponse(req.sendCheckStatus());
        return mbox;
    }

    public void create(String name) throws IOException {
        newRequest(CAtom.CREATE, new MailboxName(name)).sendCheckStatus();
    }

    public void delete(String name) throws IOException {
        newRequest(CAtom.DELETE, new MailboxName(name)).sendCheckStatus();
    }

    public void rename(String from, String to) throws IOException {
        newRequest(CAtom.RENAME, new MailboxName(from), new MailboxName(to)).sendCheckStatus();
    }

    public void subscribe(String name) throws IOException {
        newRequest(CAtom.SUBSCRIBE, new MailboxName(name)).sendCheckStatus();
    }

    public void unsubscribe(String name) throws IOException {
        newRequest(CAtom.UNSUBSCRIBE, new MailboxName(name)).sendCheckStatus();
    }

    public AppendResult append(String mbox, Flags flags, Date date, Literal data) throws IOException {
        return append(mbox, new AppendMessage(flags, date, data));
    }

    public AppendResult append(String mbox, AppendMessage... msgs) throws IOException {
        return append(mbox, Arrays.asList(msgs));
    }

    public AppendResult append(String mbox, Collection<AppendMessage> msgs) throws IOException {
        ImapRequest req = newRequest(CAtom.APPEND, new MailboxName(mbox));
        for (AppendMessage msg : msgs) {
            req.addParam(msg);
        }
        ImapResponse res = req.sendCheckStatus();
        ResponseText rt = res.getResponseText();
        return rt.getCCode() == CAtom.APPENDUID ? (AppendResult) rt.getData() : null;
    }

    public void expunge() throws IOException {
        newRequest(CAtom.EXPUNGE).sendCheckStatus();
    }

    public void uidExpunge(String seq) throws IOException {
        newUidRequest(CAtom.EXPUNGE, seq).sendCheckStatus();
    }

    public synchronized void close_mailbox() throws IOException {
        newRequest(CAtom.CLOSE).sendCheckStatus();
        mailbox = null;
        setState(State.AUTHENTICATED);
    }

    public synchronized void unselect() throws IOException {
        newRequest(CAtom.UNSELECT).sendCheckStatus();
        mailbox = null;
        setState(State.AUTHENTICATED);
    }

    public MailboxInfo status(String name, Object... params) throws IOException {
        ImapRequest req = newRequest(CAtom.STATUS, new MailboxName(name), params);
        List<MailboxInfo> results = new ArrayList<MailboxInfo>(1);
        req.setResponseHandler(new BasicResponseHandler(CAtom.STATUS, results));
        req.sendCheckStatus();
        if (results.isEmpty()) {
            throw new MailException("Missing STATUS response data");
        }
        return results.get(0);
    }

    public List<ListData> list(String ref, String mbox) throws IOException {
        return doList(CAtom.LIST, ref, mbox);
    }

    public List<ListData> lsub(String ref, String mbox) throws IOException {
        return doList(CAtom.LSUB, ref, mbox);
    }

    public boolean exists(String mbox) throws IOException {
        return !list("", mbox).isEmpty();
    }

    private List<ListData> doList(CAtom cmd, String ref, String mbox) throws IOException {
        ImapRequest req = newRequest(cmd, new MailboxName(ref), new MailboxName(mbox));
        List<ListData> results = new ArrayList<ListData>();
        req.setResponseHandler(new BasicResponseHandler(CAtom.LIST.atom(), results));
        req.sendCheckStatus();
        return results;
    }

    public char getDelimiter() throws IOException {
        if (delimiter == null) {
            List<ListData> ld = list("", "");
            delimiter = ld.isEmpty() ? 0 : ld.get(0).getDelimiter();
        }
        return delimiter;
    }

    public CopyResult copy(String seq, String mbox) throws IOException {
        ImapRequest req = newRequest(CAtom.COPY, seq, new MailboxName(mbox));
        ResponseText rt = req.sendCheckStatus().getResponseText();
        return rt.getCCode() == CAtom.COPYUID ? (CopyResult) rt.getData() : null;
    }

    public CopyResult uidCopy(String seq, String mbox) throws IOException {
        ImapRequest req = newUidRequest(CAtom.COPY, seq, new MailboxName(mbox));
        ResponseText rt = req.sendCheckStatus().getResponseText();
        return rt.getCCode() == CAtom.COPYUID ? (CopyResult) rt.getData() : null;
    }

    public void fetch(String seq, Object param, ResponseHandler handler) throws IOException {
        fetch(CAtom.FETCH.name(), seq, param, handler);
    }

    public void uidFetch(String seq, Object param, ResponseHandler handler) throws IOException {
        ImapRequest req = newUidRequest(CAtom.FETCH, seq, param);
        req.setResponseHandler(handler);
        req.sendCheckStatus();
    }

    private void fetch(String cmd, String seq, Object param, ResponseHandler handler) throws IOException {
        ImapRequest req = newRequest(cmd, seq, param);
        req.setResponseHandler(handler);
        req.sendCheckStatus();
    }

    public List<Long> getUids(String seq) throws IOException {
        final List<Long> uids = new ArrayList<Long>();
        uidFetch(seq, "UID", new FetchResponseHandler() {
            @Override
            public void handleFetchResponse(MessageData md) {
                uids.add(md.getUid());
            }
        });
        return uids;
    }

    public Map<Long, MessageData> fetch(String seq, Object param) throws IOException {
        final Map<Long, MessageData> results = new HashMap<Long, MessageData>();
        fetch(seq, param, new FetchResponseHandler(false) {
            @Override
            public void handleFetchResponse(MessageData md) {
                long msgno = md.getMsgno();
                if (msgno > 0) {
                    MessageData omd = results.get(msgno);
                    if (omd != null) {
                        omd.addFields(md);
                    } else {
                        results.put(msgno, md);
                    }
                }
            }
        });
        return results;
    }

    public MessageData fetch(long msgno, Object param) throws IOException {
        return fetch(String.valueOf(msgno), param).get(msgno);
    }

    public Map<Long, MessageData> uidFetch(String seq, Object param) throws IOException {
        final Map<Long, MessageData> results = new HashMap<Long, MessageData>();
        uidFetch(seq, param, new FetchResponseHandler(false) {
            @Override
            public void handleFetchResponse(MessageData md) {
                long uid = md.getUid();
                if (uid > 0) {
                    MessageData omd = results.get(uid);
                    if (omd != null) {
                        omd.addFields(md);
                    } else {
                        results.put(uid, md);
                    }
                }
            }
        });
        return results;
    }

    public MessageData uidFetch(long uid, Object param) throws IOException {
        return uidFetch(String.valueOf(uid), param).get(uid);
    }

    public List<Long> search(Object... params) throws IOException {
        return doSearch(CAtom.SEARCH.name(), params);
    }

    public List<Long> uidSearch(Object... params) throws IOException {
        return doSearch("UID SEARCH", params);
    }

    @SuppressWarnings("unchecked")
    private List<Long> doSearch(String cmd, Object... params) throws IOException {
        final List<Long> results = new ArrayList<Long>();
        ImapRequest req = newRequest(cmd, params);
        req.setResponseHandler(new ResponseHandler() {
            @Override
            public void handleResponse(ImapResponse res) {
                if (res.getCCode() == CAtom.SEARCH) {
                    results.addAll((List<Long>) res.getData());
                }
            }
        });
        req.sendCheckStatus();
        return results;
    }

    public void store(String seq, String item, Object flags) throws IOException {
        store(seq, item, flags, null);
    }

    public void store(String seq, String item, Object flags, ResponseHandler handler) throws IOException {
        ImapRequest req = newRequest(CAtom.STORE, seq, item, flags);
        req.setResponseHandler(handler);
        req.sendCheckStatus();
    }

    public void uidStore(String seq, String item, Object flags) throws IOException {
        uidStore(seq, item, flags, null);
    }

    public void uidStore(String seq, String item, Object flags, ResponseHandler handler) throws IOException {
        ImapRequest req = newUidRequest(CAtom.STORE, seq, item, flags);
        req.setResponseHandler(handler);
        req.sendCheckStatus();
    }

    public ImapRequest newRequest(CAtom cmd, Object... params) {
        return new ImapRequest(this, cmd.atom(), params);
    }

    public ImapRequest newRequest(Atom cmd, Object... params) {
        return new ImapRequest(this, cmd, params);
    }

    public ImapRequest newRequest(String cmd, Object... params) {
        return new ImapRequest(this, new Atom(cmd), params);
    }

    public ImapRequest newUidRequest(CAtom cmd, Object... params) {
        return newRequest("UID " + cmd.toString(), params);
    }

    public ImapCapabilities getCapabilities() {
        return capabilities;
    }

    public MailboxInfo getMailboxInfo() {
        // Make sure we return a copy of the actual mailbox since it can
        // be modified in-place in response to unsolicited messages from
        // the server.
        return mailbox != null ? new MailboxInfo(mailbox) : null;
    }

    public boolean hasCapability(String cap) {
        return capabilities != null && capabilities.hasCapability(cap);
    }

    public boolean hasIdle() {
        return hasCapability(ImapCapabilities.IDLE);
    }

    public boolean hasUnselect() {
        return hasCapability(ImapCapabilities.UNSELECT);
    }

    public boolean hasMechanism(String method) {
        return hasCapability("AUTH=" + method);
    }

    public boolean hasUidPlus() {
        return hasCapability(ImapCapabilities.UIDPLUS);
    }

    // Called from ImapRequest
    synchronized ImapResponse sendRequest(ImapRequest req) throws IOException {
        if (isClosed()) {
            throw new IOException("Connection is closed");
        }
        if (request != null) {
            throw new IllegalStateException("Request already pending");
        }
        if (req.isIdle()) {
            return sendIdle(req);
        }
        request = req;
        try {
            try {
                req.write(getImapOutputStream());
            } catch (LiteralException e) {
                return e.res;
            }
            // Wait for final response, handle continuation response
            while (true) {
                ImapResponse res = waitForResponse();
                if (res.isTagged()) {
                    return res;
                }
                assert res.isContinuation();
                if (!req.isAuthenticate()) {
                    throw req.failed("Unexpected continuation response");
                }
                processContinuation(res.getResponseText().getText());
            }
        } finally {
            request = null;
        }
    }

    public synchronized boolean isIdling() {
        return request != null && request.isIdle();
    }

    private ImapResponse sendIdle(ImapRequest req) {
        request = req;
        try {
            req.write(getImapOutputStream());
            ImapResponse res = waitForResponse();
            if (res.isTagged()) {
                return res;
            }
            assert res.isContinuation();
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    idleHandler();
                }
            });
            t.setName("IMAP IDLE thread");
            t.setDaemon(true);
            t.start();
        } catch (IOException e) {
            request = null;
        }
        return null;
    }

    private void idleHandler() {
        try {
            ImapResponse res = waitForResponse();
            if (res.isContinuation()) {
                throw new IOException("Unexpected continuation response");
            }
            assert res.isTagged();
        } catch (SocketTimeoutException e) {
            getLogger().debug("Timed-out during IDLE", e);
        } catch (IOException e) {
            if (!isClosed()) {
                getLogger().error("IDLE failed", e);
            }
        }
        synchronized (this) {
            request = null;
            notifyAll();
        }
    }

    public synchronized boolean stopIdle() throws IOException {
        if (isIdling()) {
            ImapOutputStream out = getImapOutputStream();
            out.writeLine("DONE");
            out.flush();
            long waitTime = (getImapConfig().getReadTimeout() > 0 ? getImapConfig().getReadTimeout() : 30)
                    * Constants.MILLIS_PER_SECOND;
            while (isIdling()) {
                long waitStart = System.currentTimeMillis();
                try {
                    wait(waitTime); //give server a chance to handle DONE normally
                } catch (InterruptedException e) {
                    // Ignore
                }
                waitTime = waitTime - (System.currentTimeMillis() - waitStart);
                if (waitTime <= 0 && isIdling()) {
                    close();
                    return false; //close and return false so connection is not reused
                }
            }
        }
        return !isIdling();
    }

    private ImapOutputStream getImapOutputStream() {
        return (ImapOutputStream) mailOut;
    }

    // Called from ImapRequest
    void writeLiteral(Literal lit) throws IOException {
        boolean lp = getImapConfig().isUseLiteralPlus() && hasCapability(ImapCapabilities.LITERAL_PLUS);
        ImapOutputStream out = (ImapOutputStream) mailOut;
        lit.writePrefix(out, lp);
        if (!lp) {
            out.flush();
            ImapResponse res = waitForResponse();
            if (!res.isContinuation()) {
                assert res.isTagged();
                throw new LiteralException(res);
            }
        }
        lit.writeData(out);
    }

    public ImapConfig getImapConfig() {
        return (ImapConfig) config;
    }

    // Exception thrown if we get an unexpected response to literal data
    private static class LiteralException extends IOException {
        final ImapResponse res;

        LiteralException(ImapResponse res) {
            this.res = res;
        }
    }

    private boolean isShutdown() {
        return isClosed() || isLogout();
    }

    /*
    * Read and process responses until next tagged or continuation response
    * has been received. Throws EOFException if end of stream has been
    * reached.
    */
    private ImapResponse waitForResponse() throws IOException {
        ImapResponse res;
        do {
            res = readResponse();
        } while (processResponse(res));
        return res;
    }

    private ImapResponse readResponse() throws IOException {
        try {
            return ImapResponse.read((ImapInputStream) mailIn);
        } catch (ParseException pe) {
            //read rest of the line so TraceInputStream dumps it for debugging
            mailIn.readLine();
            mailIn.trace();
            throw pe;
        }
    }

    /*
    * Process IMAP response. Returns true if this is not a final response and
    * reading should continue. Otherwise, returns false if tagged, untagged BAD,
    * or continuation response.
    */
    private synchronized boolean processResponse(ImapResponse res) throws IOException {
        if (res.isUntagged() && res.isBAD()) {
            getLogger().error("Untagged BAD response: " + res);
            return true;
        }
        if (res.isContinuation() || res.isUntagged() && res.isBAD()) {
            return false;
        }
        if (res.isUntagged()) {
            processUntagged(res);
            res.dispose(); // Clean up associated literal data

        }
        if (res.isOK()) {
            ResponseText rt = res.getResponseText();
            Atom code = rt.getCode();
            if (code != null && code.getCAtom() == CAtom.CAPABILITY) {
                capabilities = (ImapCapabilities) rt.getData();
            }
        } else if (res.getCCode() == CAtom.CAPABILITY) {
            capabilities = (ImapCapabilities) res.getData();
        } else if (mailbox != null && !request.isSelectOrExamine()) {
            mailbox.handleResponse(res);
        }
        return res.isUntagged();
    }

    /*
     * Process untagged response. Returns true if request handler processed
     * response otherwise returns false.
     */
    private boolean processUntagged(ImapResponse res) throws IOException {
        ResponseHandler handler = request.getResponseHandler();
        if (handler != null) {
            try {
                handler.handleResponse(res);
            } catch (Throwable e) {
                throw new MailException("Exception in response handler", e);
            }
        }
        return false;
    }

    public String newTag() {
        Formatter fmt = new Formatter();
        fmt.format(TAG_FORMAT, tagCount.incrementAndGet());
        return fmt.toString();
    }

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

}