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

com.yahoo.imapnio.async.internal.ImapAsyncSessionImpl Maven / Gradle / Ivy

The newest version!
package com.yahoo.imapnio.async.internal;

import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.util.Collection;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import javax.annotation.Nonnull;

import org.slf4j.Logger;

import com.sun.mail.imap.protocol.IMAPResponse;
import com.yahoo.imapnio.async.client.ImapAsyncClient;
import com.yahoo.imapnio.async.client.ImapAsyncSession;
import com.yahoo.imapnio.async.client.ImapFuture;
import com.yahoo.imapnio.async.exception.ImapAsyncClientException;
import com.yahoo.imapnio.async.exception.ImapAsyncClientException.FailureType;
import com.yahoo.imapnio.async.netty.ImapClientCommandRespHandler;
import com.yahoo.imapnio.async.netty.ImapCommandChannelEventProcessor;
import com.yahoo.imapnio.async.request.CompressCommand;
import com.yahoo.imapnio.async.request.IdleCommand;
import com.yahoo.imapnio.async.request.ImapRequest;
import com.yahoo.imapnio.async.response.ImapAsyncResponse;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.compression.JdkZlibDecoder;
import io.netty.handler.codec.compression.JdkZlibEncoder;
import io.netty.handler.codec.compression.ZlibWrapper;
import io.netty.handler.timeout.IdleStateEvent;

/**
 * This class establishes a session between imap server and sends command to server with async future.
 */
public class ImapAsyncSessionImpl implements ImapAsyncSession, ImapCommandChannelEventProcessor, ChannelFutureListener {

    /** Label for command type, used in exception message. */
    private static final String CMD_TYPE = ",cmdType:";

    /** Label for command start tye. */
    private static final String CMD_START_TIME = ",cmdStartTime:";

    /** Label for command type, used in exception message. */
    private static final String CMD_TAG = ",cmdTag:";

    /** Error record for the session, first {} is sessionId, 2nd user information. */
    private static final String SESSION_LOG_REC = "[{},{}] {}";

    /** Error record for the session, first {} is sessionId, 2nd user information. */
    private static final String SESSION_LOG_WITH_EXCEPTION = "[{},{}]";

    /** Debug log record for server, first {} is sessionId, 2nd user information, 3rd for server message. */
    private static final String SERVER_LOG_REC = "[{},{}] S:{}";

    /** Debug log record for client, first {} is sessionId, 2nd user information, 3rd for client message. */
    private static final String CLIENT_LOG_REC = "[{},{}] C:{}";

    /** Space character. */
    static final char SPACE = ' ';

    /** Space character length in bytes. */
    static final int SPACE_LENGTH = 1;

    /** Tag prefix. */
    private static final char A = 'a';

    /** Deflater handler name for enabling server compress. */
    private static final String ZLIB_DECODER = "DEFLATER";

    /** Inflater handler name for enabling server compress. */
    private static final String ZLIB_ENCODER = "INFLATER";

    /** The Netty channel object. */
    private AtomicReference channelRef = new AtomicReference();

    /** Session Id. */
    private long sessionId;

    /** Instance that stores the client context, we will call toString() of it. */
    @Nonnull
    private Object sessionCtx;

    /** Clock instance. */
    private Clock clock;

    /** Producer queue. */
    private ConcurrentLinkedQueue requestsQueue;

    /** Logger. */
    private Logger logger;

    /** Debug mode. */
    private AtomicReference debugModeRef = new AtomicReference();

    /** Sequence number for tag. */
    private AtomicLong tagSequence;

    /**
     * This class handles and manages response from server and determines whether the job for this request is done. When the request is done, it sets
     * the future to done and returns the appropriate status to caller via handleResponse method.
     *
     * @param  the data type for next command after continuation.
     */
    private static class ImapCommandEntry {
        /**
         * State of the command in its life cycle.
         */
        public enum CommandState {
            /** Request (command line) is in preparation to be generated and sent, but not yet sent to server. */
            REQUEST_IN_PREPARATION,
            /** Request (command line) is confirmed sent to the the server. */
            REQUEST_SENT,
            /** Server done with responses for the given client request. Server is not obligated to send more responses per given request. */
            RESPONSES_DONE
        }

        /** An Imap command. */
        @Nonnull
        private final ImapRequest cmd;

        /** The state of command. */
        @Nonnull
        private CommandState state;

        /** List of response lines. */
        @Nonnull
        private final ConcurrentLinkedQueue responses;

        /** ImapCommandFuture. */
        @Nonnull
        private final ImapFuture future;

        /** The tag for this command. */
        @Nonnull
        private final String tag;

        /** Number of bytes in request. */
        private int requestTotalBytes;

        /** Number of bytes in response. */
        private int responseTotalBytes;

        /** Request start time. */
        private long requestStartTimeInMillis;

        /**
         * Initializes a newly created {@link ImapCommandEntry} object so that it can handle the command responses and determine whether the request
         * is done.
         *
         * @param cmd ImapRequest instance
         * @param future ImapFuture instance
         * @param tag the tag associated with this command
         * @param requestTotalBytes request total bytes
         * @param requestStartTimeInMillis request start time
         */
        ImapCommandEntry(@Nonnull final ImapRequest cmd, @Nonnull final ImapFuture future, @Nonnull final String tag,
                final int requestTotalBytes, final long requestStartTimeInMillis) {
            this.cmd = cmd;
            this.state = CommandState.REQUEST_IN_PREPARATION;
            this.responses = (cmd.getStreamingResponsesQueue() != null) ? cmd.getStreamingResponsesQueue()
                    : new ConcurrentLinkedQueue();
            this.future = future;
            this.tag = tag;
            this.requestTotalBytes = requestTotalBytes;
            this.responseTotalBytes = 0;
            this.requestStartTimeInMillis = requestStartTimeInMillis;
        }

        /**
         * Sets the state of the command.
         *
         * @param state the target state to set
         */
        public void setState(@Nonnull final CommandState state) {
            this.state = state;
        }

        /**
         * @return the state of the command
         */
        public CommandState getState() {
            return state;
        }

        /**
         * @return the responses list
         */
        public Collection getResponses() {
            return responses;
        }

        /**
         * @return the future for the imap command
         */
        public ImapFuture getFuture() {
            return future;
        }

        /**
         * @return the imap command
         */
        public ImapRequest getRequest() {
            return cmd;
        }

        /**
         * @return the tag for this imap command
         */
        public String getTag() {
            return tag;
        }

        /**
         * Populates the entry information to the given StringBuilder.
         *
         * @param sb StringBuilder instance to output the entry information
         */
        public void debugInfo(@Nonnull final StringBuilder sb) {
            sb.append(CMD_TAG).append(tag).append(CMD_TYPE).append(getRequest().getCommandType()).append(CMD_START_TIME)
                    .append(getRequestStartTimeInMillis());
        }

        /**
         * Records number of bytes in request.
         *
         * @param length content length
         */
        public void recordRequestBytes(final int length) {
            requestTotalBytes += length;
        }

        /**
         * @return number of bytes in request.
         */
        public int getRequestTotalBytes() {
            return requestTotalBytes;
        }

        /**
         * Records number of bytes in response.
         *
         * @param length content length
         */
        public void recordResponseBytes(final int length) {
            responseTotalBytes += length;
        }

        /**
         * @return request start time.
         */
        public long getRequestStartTimeInMillis() {
            return requestStartTimeInMillis;
        }

        /**
         * @return number of bytes in response.
         */
        public int getResponseTotalBytes() {
            return responseTotalBytes;
        }
    }

    /**
     * Initializes an imap session that supports async operations.
     *
     * @param clock Clock instance
     * @param channel Channel object established for this session
     * @param logger Logger object
     * @param debugMode Flag for debugging
     * @param sessionId the session id
     * @param pipeline the ChannelPipeline object
     * @param sessionCtx context for client to store information
     */
    public ImapAsyncSessionImpl(@Nonnull final Clock clock, @Nonnull final Channel channel, @Nonnull final Logger logger,
            @Nonnull final DebugMode debugMode, final long sessionId, final ChannelPipeline pipeline, @Nonnull final Object sessionCtx) {
        this.channelRef.set(channel);
        this.clock = clock;
        this.logger = logger;
        this.debugModeRef.set(debugMode);
        this.sessionId = sessionId;
        this.requestsQueue = new ConcurrentLinkedQueue();
        this.tagSequence = new AtomicLong(0);
        this.sessionCtx = sessionCtx;
        pipeline.addLast(ImapClientCommandRespHandler.HANDLER_NAME, new ImapClientCommandRespHandler(this));
    }

    /**
     * @return returns the user information
     */
    private String getUserInfo() {
        return sessionCtx.toString();
    }

    /**
     * Generates a new tag.
     *
     * @return the new tag that was not used
     */
    private String getNextTag() {
        return new StringBuilder().append(A).append(tagSequence.incrementAndGet()).toString();
    }

    /**
     * @return true if debugging is enabled either for the session or for all sessions
     */
    private boolean isDebugEnabled() {
        // when trace is enabled, log for all sessions
        // when debug is enabled && session debug is on, we print specific session
        return logger.isTraceEnabled() || (logger.isDebugEnabled() && debugModeRef.get() == DebugMode.DEBUG_ON);
    }

    @Override
    public void setDebugMode(@Nonnull final DebugMode newOption) {
        this.debugModeRef.set(newOption);
    }

    @Override
    public ImapFuture execute(@Nonnull final ImapRequest command) throws ImapAsyncClientException {
        if (isChannelClosed()) { // fail fast instead of entering to sendRequest() to fail
            throw new ImapAsyncClientException(FailureType.OPERATION_PROHIBITED_ON_CLOSED_CHANNEL, sessionId, sessionCtx);
        }
        if (!requestsQueue.isEmpty()) { // when prior command is in process, do not allow the new one
            throw new ImapAsyncClientException(FailureType.COMMAND_NOT_ALLOWED, sessionId, sessionCtx);
        }

        final ImapFuture cmdFuture = new ImapFuture();
        final String tag = getNextTag();
        final int requestTotalBytes = tag.getBytes(StandardCharsets.US_ASCII).length + SPACE_LENGTH + command.getCommandLineBytes().readableBytes();
        requestsQueue.add(new ImapCommandEntry(command, cmdFuture, tag, requestTotalBytes, clock.millis()));

        final ByteBuf buf = Unpooled.buffer();

        buf.writeBytes(tag.getBytes(StandardCharsets.US_ASCII));
        buf.writeByte(SPACE);
        buf.writeBytes(command.getCommandLineBytes());

        sendRequest(buf, command);

        return cmdFuture;
    }

    @Override
    public  ImapFuture startCompression() throws ImapAsyncClientException {
        final ImapFuture future = execute(new CompressCommand());
        return future;
    }

    /**
     * @return true if channel is closed; false otherwise
     */
    @Override
    public boolean isChannelClosed() {
        return !channelRef.get().isActive();
    }

    /**
     * Sends the given request to server when being called.
     *
     * @param request the message of the request
     * @param command the imap command
     * @throws ImapAsyncClientException when channel is closed
     */
    private void sendRequest(@Nonnull final ByteBuf request, @Nonnull final ImapRequest command) throws ImapAsyncClientException {
        if (isDebugEnabled()) {
            // log given request if it not sensitive, otherwise log the debug data decided by command
            logger.debug(CLIENT_LOG_REC, sessionId, getUserInfo(),
                    (!command.isCommandLineDataSensitive()) ? request.toString(StandardCharsets.UTF_8) : command.getDebugData());
        }
        if (isChannelClosed()) {
            throw new ImapAsyncClientException(FailureType.OPERATION_PROHIBITED_ON_CLOSED_CHANNEL, sessionId, sessionCtx);
        }

        // ChannelPromise is the suggested ChannelFuture that allows caller to setup listener before the action is made
        // this is useful for light-speed operation.
        final Channel channel = channelRef.get();
        final ChannelPromise writeFuture = channel.newPromise();
        writeFuture.addListener(this); // "this" listens to write future done in operationComplete() to handle exception in writing.
        channel.writeAndFlush(request, writeFuture);
    }

    @Override
    public ImapFuture terminateCommand(@Nonnull final ImapRequest command) throws ImapAsyncClientException {
        if (requestsQueue.isEmpty()) {
            throw new ImapAsyncClientException(FailureType.COMMAND_NOT_ALLOWED, sessionId, sessionCtx);
        }

        final ImapCommandEntry entry = requestsQueue.peek();
        sendRequest(entry.getRequest().getTerminateCommandLine(), command);
        return entry.getFuture();
    }

    /**
     * Listens to write to server complete future.
     *
     * @param future ChannelFuture instance to check whether the future has completed successfully
     */
    @Override
    public void operationComplete(final ChannelFuture future) {
        final ImapCommandEntry entry = requestsQueue.peek();
        if (entry != null) {
            // set the state to REQUEST_SENT regardless success or not
            entry.setState(ImapCommandEntry.CommandState.REQUEST_SENT);
        }

        if (!future.isSuccess()) { // failed to write to server
            handleChannelException(new ImapAsyncClientException(FailureType.WRITE_TO_SERVER_FAILED, future.cause(), sessionId, sessionCtx));
        }
    }

    @Override
    public void handleChannelClosed() {
        if (isDebugEnabled()) {
            logger.debug(SESSION_LOG_REC, sessionId, getUserInfo(), "Session is confirmed closed.");
        }

        final StringBuilder sb = new StringBuilder(getUserInfo());
        final ImapCommandEntry curEntry = getFirstEntry();
        if (curEntry != null) {
            curEntry.debugInfo(sb);
        }

        // set the future done if there is any
        requestDoneWithException(new ImapAsyncClientException(FailureType.CHANNEL_DISCONNECTED, sessionId, sb.toString()));
    }

    /**
     * Removes the first entry in the queue and calls ImapRequest.cleanup.
     *
     * @return the removed entry, returns null if queue is empty
     */
    private ImapCommandEntry removeFirstEntry() {
        if (requestsQueue.isEmpty()) {
            return null;
        }

        final ImapCommandEntry entry = requestsQueue.poll();
        // clean up the command since it is done regardless success or fail
        entry.getRequest().cleanup();
        return entry;
    }

    /**
     * @return the current in-progress request without removing it
     */
    private ImapCommandEntry getFirstEntry() {
        return (requestsQueue.isEmpty()) ? null : requestsQueue.peek();
    }

    /**
     * Sets the future done when command is executed unsuccessfully.
     *
     * @param cause the cause of why the operation fails
     */
    private void requestDoneWithException(@Nonnull final ImapAsyncClientException cause) {
        final ImapCommandEntry entry = removeFirstEntry();
        if (entry == null) {
            return;
        }

        // log at error level
        if (isDebugEnabled()) {
            logger.debug(SESSION_LOG_WITH_EXCEPTION, sessionId, getUserInfo(), cause);
        }
        entry.getFuture().done(cause);

        // close session when encountering channel exception since the health of session is frail/unknown.
        close();
    }

    @Override
    public void handleChannelException(@Nonnull final Throwable cause) {
        final StringBuilder sb = new StringBuilder(getUserInfo());
        final ImapCommandEntry curEntry = getFirstEntry();
        if (curEntry != null) {
            curEntry.debugInfo(sb);
        }
        requestDoneWithException(new ImapAsyncClientException(FailureType.CHANNEL_EXCEPTION, cause, sessionId, sb.toString()));
    }

    @Override
    public void handleIdleEvent(@Nonnull final IdleStateEvent idleEvent) {
        final ImapCommandEntry curEntry = getFirstEntry();
        // only throws channel timeout when a request is sent and we are waiting for the responses to come
        if (curEntry == null || curEntry.getState() != ImapCommandEntry.CommandState.REQUEST_SENT || curEntry.getRequest() instanceof IdleCommand) {
            return;
        }

        // error out for any other commands sent but server is not responding
        final StringBuilder sb = new StringBuilder(getUserInfo());
        curEntry.debugInfo(sb);

        requestDoneWithException(new ImapAsyncClientException(FailureType.CHANNEL_TIMEOUT, sessionId, sb.toString()));
    }

    @Override
    public  void handleChannelResponse(@Nonnull final IMAPResponse serverResponse) {
        final ImapCommandEntry curEntry = getFirstEntry();
        if (curEntry == null) {
            return;
        }

        final ImapRequest currentCmd = curEntry.getRequest();
        final Collection responses = curEntry.getResponses();
        responses.add(serverResponse);
        curEntry.recordResponseBytes(serverResponse.toString().getBytes(StandardCharsets.US_ASCII).length);

        if (isDebugEnabled()) { // logging all server responses when enabled
            logger.debug(SERVER_LOG_REC, sessionId, getUserInfo(), serverResponse.toString());
        }

        // server sends continuation message (+) for next request
        if (serverResponse.isContinuation()) {
            try {
                curEntry.setState(ImapCommandEntry.CommandState.RESPONSES_DONE);
                final ByteBuf cmdAfterContinue = currentCmd.getNextCommandLineAfterContinuation(serverResponse);
                if (cmdAfterContinue == null) {
                    return; // no data from client after continuation, we leave, this is for Idle
                }
                curEntry.setState(ImapCommandEntry.CommandState.REQUEST_IN_PREPARATION); // preparing to send request
                curEntry.recordRequestBytes(cmdAfterContinue.readableBytes());
                sendRequest(cmdAfterContinue, currentCmd);

            } catch (final ImapAsyncClientException | RuntimeException e) { // when encountering an error on building request from client
                requestDoneWithException(
                        new ImapAsyncClientException(ImapAsyncClientException.FailureType.CHANNEL_EXCEPTION, e, sessionId, sessionCtx));
            }
            return;

        } else if (serverResponse.isTagged() && curEntry.getTag().equals(serverResponse.getTag())) {
            // If this is a matching command completion response, we are done
            try {
                curEntry.setState(ImapCommandEntry.CommandState.RESPONSES_DONE);
                if (currentCmd instanceof CompressCommand && serverResponse.isOK()) {
                    // check whether channel is closed before dereferencing.
                    if (isChannelClosed()) {
                        requestDoneWithException(
                                new ImapAsyncClientException(FailureType.OPERATION_PROHIBITED_ON_CLOSED_CHANNEL, sessionId, sessionCtx));
                        return;
                    }

                    final Channel ch = channelRef.get();
                    final ChannelPipeline pipeline = ch.pipeline();
                    final JdkZlibDecoder decoder = new JdkZlibDecoder(ZlibWrapper.NONE);
                    final JdkZlibEncoder encoder = new JdkZlibEncoder(ZlibWrapper.NONE, 5);
                    if (pipeline.get(ImapAsyncClient.SSL_HANDLER) == null) {
                        // no SSL handler, deflater/enflater has to be first
                        pipeline.addFirst(ZLIB_DECODER, decoder);
                        pipeline.addFirst(ZLIB_ENCODER, encoder);
                    } else {
                        pipeline.addAfter(ImapAsyncClient.SSL_HANDLER, ZLIB_DECODER, decoder);
                        pipeline.addAfter(ImapAsyncClient.SSL_HANDLER, ZLIB_ENCODER, encoder);
                    }
                }
                // see rfc3501, page 63 for details, since we always give a tagged command, response completion should be the first tagged response
                final long totalTimeElapsedInMillis = clock.millis() - curEntry.getRequestStartTimeInMillis();
                final ImapAsyncResponse doneResponse = new ImapAsyncResponse(curEntry.getRequest().getCommandType(), curEntry.getRequestTotalBytes(),
                        curEntry.getResponseTotalBytes(), responses, totalTimeElapsedInMillis);
                removeFirstEntry();
                curEntry.getFuture().done(doneResponse);
                return;
            } catch (final RuntimeException e) {
                requestDoneWithException(
                        new ImapAsyncClientException(ImapAsyncClientException.FailureType.CHANNEL_EXCEPTION, e, sessionId, sessionCtx));
            }
        }

        // none-tagged server responses if reaching here
    }

    @Override
    public ImapFuture close() {
        final ImapFuture closeFuture = new ImapFuture();
        if (isChannelClosed()) {
            closeFuture.done(Boolean.TRUE);
        } else {
            if (isDebugEnabled()) {
                logger.debug(SESSION_LOG_REC, sessionId, getUserInfo(), "Closing the session via close().");
            }
            final Channel channel = channelRef.get();
            final ChannelPromise channelPromise = channel.newPromise();
            final ImapChannelClosedListener channelClosedListener = new ImapChannelClosedListener(closeFuture);
            channelPromise.addListener(channelClosedListener);
            // this triggers handleChannelDisconnected() hence no need to handle queue here. We use close() instead of disconnect() to ensure it is
            // clearly a close action regardless TCP or UDP
            channel.close(channelPromise);
        }
        return closeFuture;
    }

    /**
     * Listener for channel close event done.
     */
    class ImapChannelClosedListener implements ChannelFutureListener {

        /** Future for the ImapAsyncSession client. */
        private ImapFuture imapSessionCloseFuture;

        /**
         * Initializes a channel close listener with ImapFuture instance.
         *
         * @param imapFuture the future for caller of @{link ImapAsyncSession} close method
         */
        ImapChannelClosedListener(final ImapFuture imapFuture) {
            this.imapSessionCloseFuture = imapFuture;
        }

        @Override
        public void operationComplete(@Nonnull final ChannelFuture future) {
            if (future.isSuccess()) {
                imapSessionCloseFuture.done(Boolean.TRUE);
            } else {
                imapSessionCloseFuture
                        .done(new ImapAsyncClientException(FailureType.CLOSING_CONNECTION_FAILED, future.cause(), sessionId, sessionCtx));
            }
        }

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy