diff --git a/core/src/main/java/com/yahoo/imapnio/async/client/CustomImapAsyncClient.java b/core/src/main/java/com/yahoo/imapnio/async/client/CustomImapAsyncClient.java new file mode 100644 index 0000000..aa4cfd7 --- /dev/null +++ b/core/src/main/java/com/yahoo/imapnio/async/client/CustomImapAsyncClient.java @@ -0,0 +1,293 @@ +package com.yahoo.imapnio.async.client; + + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.yahoo.imapnio.async.client.ImapAsyncSession.DebugMode; +import com.yahoo.imapnio.async.exception.ImapAsyncClientException; +import com.yahoo.imapnio.async.exception.ImapAsyncClientException.FailureType; +import com.yahoo.imapnio.async.internal.ImapAsyncSessionImpl; +import com.yahoo.imapnio.async.netty.ImapClientConnectHandler; +import com.yahoo.imapnio.client.ImapClientRespReader; +import com.yahoo.imapnio.command.ImapClientRespDecoder; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.JdkSslContext; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.GenericFutureListener; + +/** + * Custom imap async client. + * + * @author davisthomas + * + */ + +public class CustomImapAsyncClient extends ImapAsyncClient { + + /** Literal for imaps. */ + private static final String IMAPS = "imaps"; + + /** Handler name for ssl handler. */ + public static final String SSL_HANDLER = "sslHandler"; + + /** Handler name for idle sate handler. */ + private static final String IDLE_STATE_HANDLER_NAME = "idlestateHandler"; + + /** Handler name for string decoder. */ + private static final String IMAP_LINE_DECODER_HANDLER_NAME = "ImapClientRespReader"; + + /** Handler name for string decoder. */ + private static final String STRING_DECODER_HANDLER_NAME = "decoder"; + + /** Handler name for string encoder. */ + private static final String STRING_ENCODER_HANDLER_NAME = "encoder"; + + /** Handler name for string encoder. */ + private static final String STRING_IMAP_MSG_RESPONSE_NAME = "ImapClientRespDecoder"; + + /** Debug record. */ + private static final String CONNECT_RESULT_REC = "[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"; + + /** Event loop group that will serve all channels for IMAP client. */ + private final EventLoopGroup group; + + /** Clock instance. */ + @Nonnull + private final Clock clock; + + /** Client context not available. */ + private static final String NA_CLIENT_CONTEXT = "NA"; + + /** logger for sending error, warning, info, debug to the log file. */ + @Nonnull + private final Logger logger; + + /** The Netty bootstrap. */ + private final Bootstrap bootstrap; + + /** Counter for session. */ + private final AtomicLong sessionCount = new AtomicLong(1); + + /** + * Constructs a NIO based IMAP client. + * + * @param numOfThreads number of threads to be used by IMAP client + * @throws SSLException when encountering an error to create a SslContext for this client + */ + public CustomImapAsyncClient(final int numOfThreads) throws SSLException { + this(Clock.systemUTC(), new Bootstrap(), new NioEventLoopGroup(numOfThreads), LoggerFactory.getLogger(ImapAsyncClient.class)); + } + + /** + * Constructs a NIO based IMAP client. + * + * @param clock Clock instance + * @param bootstrap a {@link Bootstrap} instance that makes it easy to bootstrap a {@link Channel} to use for clients + * @param group an @{link EventLoopGroup} instance allowing registering {@link Channel}s for processing later selection during the event loop + * @param logger Logger instance + */ + CustomImapAsyncClient(final Clock clock, final Bootstrap bootstrap, final EventLoopGroup group, final Logger logger) { + super(clock, bootstrap, group, logger); + this.bootstrap = bootstrap; + this.logger = logger; + this.clock = clock; + this.group = group; + } + + /** + * This class initialized the pipeline with the right handlers. + */ + final class ImapClientChannelInitializer extends ChannelInitializer { + /** Read timeout for channel. */ + private int imapReadTimeoutValue; + + /** Unit for IdleStateHandler parameters. */ + private TimeUnit timeUnit; + + /** + * Initializes {@link ImapClientChannelInitializer} with the read time out value. + * + * @param imapReadTimeoutValue timeout value for server not responding after write command is sent + * @param unit unit of time + */ + private ImapClientChannelInitializer(final int imapReadTimeoutValue, final TimeUnit unit) { + this.imapReadTimeoutValue = imapReadTimeoutValue; + this.timeUnit = unit; + } + + @Override + protected void initChannel(final SocketChannel ch) { + final ChannelPipeline pipeline = ch.pipeline(); + + // setting all idle timeout to ensure event will only be triggered when both read and write not happened for the given time + pipeline.addLast(IDLE_STATE_HANDLER_NAME, new IdleStateHandler(0, 0, imapReadTimeoutValue, timeUnit)); // duplex + pipeline.addLast(IMAP_LINE_DECODER_HANDLER_NAME, new ImapClientRespReader(Integer.MAX_VALUE)); // inbound + pipeline.addLast(STRING_DECODER_HANDLER_NAME, new StringDecoder(StandardCharsets.US_ASCII)); // inbound + pipeline.addLast(STRING_ENCODER_HANDLER_NAME, new StringEncoder(StandardCharsets.US_ASCII)); // outbound + pipeline.addLast(STRING_IMAP_MSG_RESPONSE_NAME, new ImapClientRespDecoder()); // inbound to convert to IMAPResponse + } + } + + /** + * Connects to the remote server asynchronously and returns a future for the ImapSession if connection is established. + ** + * @param serverUri IMAP server URI + * @param config configuration to be used for this session/connection + * @param localAddress the local network interface to us + * @param sniNames Server Name Indication names list + * @param logOpt session logging option for the session to be created + * @param sessionCtx context associated with the session created. Its toString() will be called upon displaying exception or debug logging + * @param jdkSslContext a pre-configured {@link SSLContext} which uses JDK's SSL/TLS implementation + * @return the ChannelFuture object + */ + @Override + public Future createSession(@Nonnull final URI serverUri, @Nonnull final ImapAsyncSessionConfig config, + @Nullable final InetSocketAddress localAddress, @Nullable final List sniNames, @Nonnull final DebugMode logOpt, + @Nonnull final Object sessionCtx, @Nullable final SSLContext jdkSslContext) { + + final boolean isSessionDebugOn = (logOpt == DebugMode.DEBUG_ON); + // ------------------------------------------------------------ + // obtain config values + final int connectionTimeMillis = config.getConnectionTimeoutMillis(); + final int readTimeMillis = config.getReadTimeoutMillis(); + + // ------------------------------------------------------------ + // setup ChannelInitializer, handlers here need to be session-less + bootstrap.handler(new ImapClientChannelInitializer(readTimeMillis, TimeUnit.MILLISECONDS)); + + // ------------------------------------------------------------ + // connect to remote server now, setup connection timeout time before connection + bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeMillis); + + final ImapFuture sessionFuture = new ImapFuture(); + final ChannelFuture nettyConnectFuture; + if (null != localAddress) { + final InetSocketAddress remoteAddress = new InetSocketAddress(serverUri.getHost(), serverUri.getPort()); + nettyConnectFuture = bootstrap.connect(remoteAddress, localAddress); + } else { + nettyConnectFuture = bootstrap.connect(serverUri.getHost(), serverUri.getPort()); + } + + // setup listener to handle connection done event + nettyConnectFuture.addListener(new GenericFutureListener>() { + @Override + public void operationComplete(final io.netty.util.concurrent.Future future) { + if (future.isSuccess()) { + + // add the session specific handlers + final Channel ch = nettyConnectFuture.channel(); + final ChannelPipeline pipeline = ch.pipeline(); + + // ------------------------------------------------------------ + // setup session + final boolean isSSL = serverUri.getScheme().toLowerCase().equals(IMAPS); + + if (isSSL) { + SslContext sslContext; + try { + // if callers want to use their predefined SSLContext, we need to wrap it with JdkSslContext + sslContext = (jdkSslContext == null) ? SslContextBuilder.forClient().build() + : new JdkSslContext(jdkSslContext, true, ClientAuth.NONE); + } catch (final SSLException e) { + final ImapAsyncClientException ex = new ImapAsyncClientException(FailureType.CONNECTION_SSL_EXCEPTION, e); + sessionFuture.done(ex); + logger.error(CONNECT_RESULT_REC, "NA", sessionCtx.toString(), "failure", serverUri.toASCIIString(), sniNames, ex); + closeChannel(ch); + return; + } + final List serverNames = new ArrayList(); + if (null != sniNames && !sniNames.isEmpty()) { // SNI support + for (final String sni : sniNames) { + serverNames.add(new SNIHostName(sni)); + } + final SSLParameters params = new SSLParameters(); + params.setServerNames(serverNames); + + final SSLEngine engine = sslContext.newEngine(ch.alloc(), serverUri.getHost(), serverUri.getPort()); + engine.setSSLParameters(params); + pipeline.addFirst(SSL_HANDLER, new SslHandler(engine)); // in/outbound + } else { + // in/outbound + pipeline.addFirst(SSL_HANDLER, sslContext.newHandler(ch.alloc(), serverUri.getHost(), serverUri.getPort())); + } + } + + final long sessionId = sessionCount.incrementAndGet(); + sessionCount.compareAndSet(Long.MAX_VALUE - 1, 1); // roll back to 1 if reaching the max + pipeline.addLast(ImapClientConnectHandler.HANDLER_NAME, new ImapClientConnectHandler(clock, sessionFuture, + LoggerFactory.getLogger(ImapAsyncSessionImpl.class), logOpt, sessionId, sessionCtx)); + + if (logger.isTraceEnabled() || isSessionDebugOn) { + logger.debug(CONNECT_RESULT_REC, sessionId, sessionCtx.toString(), "success", serverUri.toASCIIString(), sniNames); + } + // connect action is not done until we receive the first OK response from server, so we CANNOT call it done here + } else { // failure case + final Throwable cause = future.cause(); + FailureType type = null; + if (cause instanceof UnknownHostException) { + type = FailureType.UNKNOWN_HOST_EXCEPTION; + } else if (cause instanceof ConnectTimeoutException) { + type = FailureType.CONNECTION_TIMEOUT_EXCEPTION; + } else { + type = FailureType.CONNECTION_FAILED_EXCEPTION; + } + final ImapAsyncClientException ex = new ImapAsyncClientException(type, cause); + sessionFuture.done(ex); + logger.error(CONNECT_RESULT_REC, "NA", sessionCtx.toString(), "failure", serverUri.toASCIIString(), sniNames, ex); + closeChannel(nettyConnectFuture.channel()); + } + } + }); + + return sessionFuture; + } + + /** + * Closes channel. + * + * @param channel the channel + */ + private void closeChannel(@Nullable final Channel channel) { + if (channel != null && channel.isActive()) { + channel.close(); + } + } + +} diff --git a/core/src/test/java/com/yahoo/imapnio/async/client/CustomImapAsyncClientTest.java b/core/src/test/java/com/yahoo/imapnio/async/client/CustomImapAsyncClientTest.java new file mode 100644 index 0000000..c4383ba --- /dev/null +++ b/core/src/test/java/com/yahoo/imapnio/async/client/CustomImapAsyncClientTest.java @@ -0,0 +1,872 @@ +package com.yahoo.imapnio.async.client; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.time.Clock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.yahoo.imapnio.async.client.CustomImapAsyncClient.ImapClientChannelInitializer; +import com.yahoo.imapnio.async.client.ImapAsyncSession.DebugMode; +import com.yahoo.imapnio.async.exception.ImapAsyncClientException; +import com.yahoo.imapnio.async.exception.ImapAsyncClientException.FailureType; +import com.yahoo.imapnio.async.netty.ImapClientConnectHandler; +import com.yahoo.imapnio.client.ImapClientRespReader; +import com.yahoo.imapnio.command.ImapClientRespDecoder; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.GenericFutureListener; + +/** + * Unit test for {@link CustomImapAsyncClient}. + */ +public class CustomImapAsyncClientTest { + + /** Timeout for connection. */ + private static final String SERVER_URI_STR = "imaps://one.two.three.com:993"; + + /** Server URI without SSL protocol. */ + private static final String NO_SSL_SERVER_URI_STR = "imap://one.two.three.com:993"; + + /** Time sequence for the clock tick in milliseconds. */ + private static final Long[] TIME_SEQUENCE = { 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L, 21L, 22L, + 23L, 24L, 25L, 26L, 27L, 28L, 29L, 30L, 31L, 32L, 33L, 34L, 35L, 36L, 37L, 38L, 39L, 40L, 41L, 42L, 43L, 44L, 45L, 46L, 47L, 48L, 49L, + 50L, 51L, 52L, 53L, 54L, 55L, 56L, 57L, 58L, 59L, 60L }; + + /** Clock instance. */ + private Clock clock; + + /** + * Sets up instance before each test method. + */ + @BeforeMethod + public void beforeMethod() { + clock = Mockito.mock(Clock.class); + Mockito.when(clock.millis()).thenReturn(1L, TIME_SEQUENCE); + } + + /** + * Tests createSession method when successful. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionNoLocalAddressNoSNISuccessful() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(true); + final Channel nettyChannel = Mockito.mock(Channel.class); + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(nettyChannel.pipeline()).thenReturn(nettyPipeline); + Mockito.when(nettyConnectFuture.channel()).thenReturn(nettyChannel); + Mockito.when(bootstrap.connect(Mockito.anyString(), Mockito.anyInt())).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isTraceEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = null; + + // test create session + final InetSocketAddress localAddress = null; + final URI serverUri = new URI(SERVER_URI_STR); + + final String sessCtx = "abc@nowhere.com"; + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_OFF, + sessCtx); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // test connection established and channel initialized new + final SocketChannel socketChannel = Mockito.mock(SocketChannel.class); + final ChannelPipeline socketPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(socketChannel.pipeline()).thenReturn(socketPipeline); + initializer.initChannel(socketChannel); + + // verify initChannel + final ArgumentCaptor handlerCaptor = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(socketPipeline, Mockito.times(5)).addLast(Mockito.anyString(), handlerCaptor.capture()); + Assert.assertEquals(handlerCaptor.getAllValues().size(), 5, "Unexpected count of ChannelHandler added."); + // following order should be preserved + Assert.assertEquals(handlerCaptor.getAllValues().get(0).getClass(), IdleStateHandler.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(1).getClass(), ImapClientRespReader.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(2).getClass(), StringDecoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(3).getClass(), StringEncoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(4).getClass(), ImapClientRespDecoder.class, "expected class mismatched."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 1, "number of handlers mismatched."); + Assert.assertEquals(handlerCaptorFirst.getAllValues().get(0).getClass(), SslHandler.class, "expected class mismatched."); + final CustomImapAsyncClient customClient = new CustomImapAsyncClient(100); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 1, "Unexpected count of ChannelHandler added."); + Assert.assertEquals(handlerCaptorLast.getAllValues().get(0).getClass(), ImapClientConnectHandler.class, "expected class mismatched."); + // verify logging messages + Mockito.verify(logger, Mockito.times(1)).debug(Mockito.eq("[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"), + Mockito.eq(Long.valueOf(2)), Mockito.eq("abc@nowhere.com"), Mockito.eq("success"), Mockito.eq("imaps://one.two.three.com:993"), + Mockito.eq(null)); + // call shutdown + aclient.shutdown(); + Mockito.verify(group, Mockito.times(1)).shutdownGracefully(); + } + + /** + * Tests createSession method when successful. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionNoLocalAddressNoSSLSuccessful() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(true); + final Channel nettyChannel = Mockito.mock(Channel.class); + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(nettyChannel.pipeline()).thenReturn(nettyPipeline); + Mockito.when(nettyConnectFuture.channel()).thenReturn(nettyChannel); + Mockito.when(bootstrap.connect(Mockito.anyString(), Mockito.anyInt())).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isTraceEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = null; + + // test create session + final InetSocketAddress localAddress = null; + final URI serverUri = new URI(NO_SSL_SERVER_URI_STR); + + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_OFF); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // test connection established and channel initialized new + final SocketChannel socketChannel = Mockito.mock(SocketChannel.class); + final ChannelPipeline socketPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(socketChannel.pipeline()).thenReturn(socketPipeline); + initializer.initChannel(socketChannel); + + // verify initChannel + final ArgumentCaptor handlerCaptor = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(socketPipeline, Mockito.times(5)).addLast(Mockito.anyString(), handlerCaptor.capture()); + Assert.assertEquals(handlerCaptor.getAllValues().size(), 5, "Unexpected count of ChannelHandler added."); + // following order should be preserved + Assert.assertEquals(handlerCaptor.getAllValues().get(0).getClass(), IdleStateHandler.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(1).getClass(), ImapClientRespReader.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(2).getClass(), StringDecoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(3).getClass(), StringEncoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(4).getClass(), ImapClientRespDecoder.class, "expected class mismatched."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 0, "number of handlers mismatched."); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 1, "Unexpected count of ChannelHandler added."); + Assert.assertEquals(handlerCaptorLast.getAllValues().get(0).getClass(), ImapClientConnectHandler.class, "expected class mismatched."); + // verify logging messages + Mockito.verify(logger, Mockito.times(1)).debug(Mockito.eq("[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"), + Mockito.eq(Long.valueOf(2)), Mockito.eq("NA"), Mockito.eq("success"), Mockito.eq("imap://one.two.three.com:993"), Mockito.eq(null)); + // call shutdown + aclient.shutdown(); + Mockito.verify(group, Mockito.times(1)).shutdownGracefully(); + } + + /** + * Tests createSession method when successful. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionNoLocalAddressSNIEmptySuccessful() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(true); + final Channel nettyChannel = Mockito.mock(Channel.class); + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(nettyChannel.pipeline()).thenReturn(nettyPipeline); + Mockito.when(nettyConnectFuture.channel()).thenReturn(nettyChannel); + Mockito.when(bootstrap.connect(Mockito.anyString(), Mockito.anyInt())).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isTraceEnabled()).thenReturn(false); + Mockito.when(logger.isDebugEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = new ArrayList(); + + // test create session + final InetSocketAddress localAddress = null; + final URI serverUri = new URI(SERVER_URI_STR); + + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_ON); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // test connection established and channel initialized new + final SocketChannel socketChannel = Mockito.mock(SocketChannel.class); + final ChannelPipeline socketPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(socketChannel.pipeline()).thenReturn(socketPipeline); + initializer.initChannel(socketChannel); + + // verify initChannel + final ArgumentCaptor handlerCaptor = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(socketPipeline, Mockito.times(5)).addLast(Mockito.anyString(), handlerCaptor.capture()); + Assert.assertEquals(handlerCaptor.getAllValues().size(), 5, "Unexpected count of ChannelHandler added."); + // following order should be preserved + Assert.assertEquals(handlerCaptor.getAllValues().get(0).getClass(), IdleStateHandler.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(1).getClass(), ImapClientRespReader.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(2).getClass(), StringDecoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(3).getClass(), StringEncoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(4).getClass(), ImapClientRespDecoder.class, "expected class mismatched."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 1, "number of handlers mismatched."); + Assert.assertEquals(handlerCaptorFirst.getAllValues().get(0).getClass(), SslHandler.class, "expected class mismatched."); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 1, "Unexpected count of ChannelHandler added."); + Assert.assertEquals(handlerCaptorLast.getAllValues().get(0).getClass(), ImapClientConnectHandler.class, "expected class mismatched."); + // verify if session level is on, whether debug call will be called + // verify logging messages + Mockito.verify(logger, Mockito.times(1)).debug(Mockito.eq("[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"), + Mockito.eq(Long.valueOf(2)), Mockito.eq("NA"), Mockito.eq("success"), Mockito.eq("imaps://one.two.three.com:993"), + Mockito.eq(new ArrayList())); + } + + /** + * Tests createSession method when successful with class level debug on and session level debug off. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionWithLocalAddressSniSuccessfulSessionDebugOff() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(true); + final Channel nettyChannel = Mockito.mock(Channel.class); + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(nettyChannel.pipeline()).thenReturn(nettyPipeline); + Mockito.when(nettyConnectFuture.channel()).thenReturn(nettyChannel); + Mockito.when(bootstrap.connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class))).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isDebugEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = new ArrayList(); + sniNames.add("one.two.three.com"); + // test create session + final InetSocketAddress localAddress = new InetSocketAddress("10.10.10.10", 23112); + final URI serverUri = new URI(SERVER_URI_STR); + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_OFF); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // test connection established and channel initialized new + final SocketChannel socketChannel = Mockito.mock(SocketChannel.class); + final ChannelPipeline socketPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(socketChannel.pipeline()).thenReturn(socketPipeline); + initializer.initChannel(socketChannel); + + // verify initChannel + final ArgumentCaptor handlerCaptor = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(socketPipeline, Mockito.times(5)).addLast(Mockito.anyString(), handlerCaptor.capture()); + Assert.assertEquals(handlerCaptor.getAllValues().size(), 5, "Unexpected count of ChannelHandler added."); + // following order should be preserved + Assert.assertEquals(handlerCaptor.getAllValues().get(0).getClass(), IdleStateHandler.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(1).getClass(), ImapClientRespReader.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(2).getClass(), StringDecoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(3).getClass(), StringEncoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(4).getClass(), ImapClientRespDecoder.class, "expected class mismatched."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 1, "number of handlers mismatched."); + Assert.assertEquals(handlerCaptorFirst.getAllValues().get(0).getClass(), SslHandler.class, "expected class mismatched."); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 1, "Unexpected count of ChannelHandler added."); + Assert.assertEquals(handlerCaptorLast.getAllValues().get(0).getClass(), ImapClientConnectHandler.class, "expected class mismatched."); + Mockito.verify(logger, Mockito.times(0)).debug(Mockito.anyString(), Mockito.any(Throwable.class)); + } + + /** + * @return SSLContext instance + * @throws KeyStoreException will not throw + * @throws NoSuchAlgorithmException will not throw + * @throws KeyManagementException will not throw + */ + private SSLContext buildSSLContext() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException { + final SSLContext sslContext = SSLContext.getInstance("TLS"); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init((KeyStore) null); + final TrustManager[] tm = new TrustManager[] { tmf.getTrustManagers()[0] }; + sslContext.init(null, tm, null); + return sslContext; + } + + /** + * Tests createSession method when successful with class level debug on and session level debug off. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionWithLocalAddressSniSuccessfulSessionDebugOn() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(true); + final Channel nettyChannel = Mockito.mock(Channel.class); + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(nettyChannel.pipeline()).thenReturn(nettyPipeline); + Mockito.when(nettyConnectFuture.channel()).thenReturn(nettyChannel); + Mockito.when(bootstrap.connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class))).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isDebugEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = new ArrayList(); + sniNames.add("one.two.three.com"); + // test create session + final InetSocketAddress localAddress = new InetSocketAddress("10.10.10.10", 23112); + final URI serverUri = new URI(SERVER_URI_STR); + + final String sessCtx = "someUserId"; + + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_ON, + sessCtx, buildSSLContext()); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // test connection established and channel initialized new + final SocketChannel socketChannel = Mockito.mock(SocketChannel.class); + final ChannelPipeline socketPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(socketChannel.pipeline()).thenReturn(socketPipeline); + initializer.initChannel(socketChannel); + + // verify initChannel + final ArgumentCaptor handlerCaptor = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(socketPipeline, Mockito.times(5)).addLast(Mockito.anyString(), handlerCaptor.capture()); + Assert.assertEquals(handlerCaptor.getAllValues().size(), 5, "Unexpected count of ChannelHandler added."); + // following order should be preserved + Assert.assertEquals(handlerCaptor.getAllValues().get(0).getClass(), IdleStateHandler.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(1).getClass(), ImapClientRespReader.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(2).getClass(), StringDecoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(3).getClass(), StringEncoder.class, "expected class mismatched."); + Assert.assertEquals(handlerCaptor.getAllValues().get(4).getClass(), ImapClientRespDecoder.class, "expected class mismatched."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 1, "number of handlers mismatched."); + Assert.assertEquals(handlerCaptorFirst.getAllValues().get(0).getClass(), SslHandler.class, "expected class mismatched."); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(1)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 1, "Unexpected count of ChannelHandler added."); + Assert.assertEquals(handlerCaptorLast.getAllValues().get(0).getClass(), ImapClientConnectHandler.class, "expected class mismatched."); + // verify logging messages + Mockito.verify(logger, Mockito.times(1)).debug(Mockito.eq("[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"), + Mockito.eq(Long.valueOf(2)), Mockito.eq("someUserId"), Mockito.eq("success"), Mockito.eq("imaps://one.two.three.com:993"), + Mockito.eq(Collections.singletonList("one.two.three.com"))); + } + + /** + * Tests createSession method when successful. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionNoLocalAddressConnectFailed() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(false); + final Channel nettyChannel = Mockito.mock(Channel.class); + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(nettyChannel.pipeline()).thenReturn(nettyPipeline); + Mockito.when(nettyConnectFuture.channel()).thenReturn(nettyChannel); + Mockito.when(bootstrap.connect(Mockito.anyString(), Mockito.anyInt())).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isDebugEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = null; + + // test create session + final InetSocketAddress localAddress = null; + final URI serverUri = new URI(SERVER_URI_STR); + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_OFF); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 0, "number of handlers mismatched."); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 0, "Unexpected count of ChannelHandler added."); + // verify logging messages + Mockito.verify(logger, Mockito.times(1)).error(Mockito.eq("[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"), + Mockito.eq("NA"), Mockito.eq("NA"), Mockito.eq("failure"), Mockito.eq("imaps://one.two.three.com:993"), Mockito.eq(null), + Mockito.isA(ImapAsyncClientException.class)); + + } + + /** + * Tests createSession method with unknown host exception. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionUnknownHostConnectFailed() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(false); + final Channel nettyChannel = Mockito.mock(Channel.class); + Mockito.when(nettyChannel.isActive()).thenReturn(false); + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(nettyChannel.pipeline()).thenReturn(nettyPipeline); + Mockito.when(nettyConnectFuture.channel()).thenReturn(nettyChannel); + Mockito.when(nettyConnectFuture.cause()).thenReturn(new UnknownHostException("Unknown host")); + Mockito.when(bootstrap.connect(Mockito.anyString(), Mockito.anyInt())).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isDebugEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = null; + + // test create session + final InetSocketAddress localAddress = null; + final URI serverUri = new URI(SERVER_URI_STR); + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_OFF); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 0, "number of handlers mismatched."); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 0, "Unexpected count of ChannelHandler added."); + // verify logging messages + Mockito.verify(logger, Mockito.times(1)).error(Mockito.eq("[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"), + Mockito.eq("NA"), Mockito.eq("NA"), Mockito.eq("failure"), Mockito.eq("imaps://one.two.three.com:993"), Mockito.eq(null), + Mockito.isA(ImapAsyncClientException.class)); + + Assert.assertTrue(future.isDone(), "Future should be done."); + try { + future.get(5, TimeUnit.MILLISECONDS); + Assert.fail("Should throw unknown host exception"); + } catch (final ExecutionException | InterruptedException ex) { + Assert.assertNotNull(ex, "Expect exception to be thrown."); + Assert.assertNotNull(ex.getCause(), "Expect cause."); + Assert.assertEquals(ex.getClass(), ExecutionException.class, "Class type mismatch."); + final Exception exception = (Exception) ex.getCause(); + Assert.assertEquals(exception.getClass(), ImapAsyncClientException.class, "Exception class type mismatch."); + Assert.assertNotNull(exception.getCause(), "Cause should not be null"); + Assert.assertEquals(exception.getCause().getClass(), UnknownHostException.class, "Cause should be unknown host exception"); + Assert.assertSame(exception.getCause(), nettyConnectFuture.cause(), "Cause should be same object"); + Assert.assertEquals(((ImapAsyncClientException) exception).getFailureType(), FailureType.UNKNOWN_HOST_EXCEPTION, + "Exception type should be UNKNOWN_HOST_EXCEPTION"); + } + + Mockito.verify(nettyChannel, Mockito.times(1)).isActive(); + Mockito.verify(nettyChannel, Mockito.times(0)).close(); // since channel is not active + } + + /** + * Tests createSession method with ConnectTimeout exception. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionConnectionTimeoutFailed() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(false); + final Channel nettyChannel = Mockito.mock(Channel.class); + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + Mockito.when(nettyChannel.pipeline()).thenReturn(nettyPipeline); + Mockito.when(nettyChannel.isActive()).thenReturn(true); + Mockito.when(nettyConnectFuture.channel()).thenReturn(nettyChannel); + Mockito.when(nettyConnectFuture.cause()).thenReturn(new ConnectTimeoutException("connection timed out")); + Mockito.when(bootstrap.connect(Mockito.anyString(), Mockito.anyInt())).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isDebugEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = null; + + // test create session + final InetSocketAddress localAddress = null; + final URI serverUri = new URI(SERVER_URI_STR); + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_OFF); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 0, "number of handlers mismatched."); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 0, "Unexpected count of ChannelHandler added."); + // verify logging messages + Mockito.verify(logger, Mockito.times(1)).error(Mockito.eq("[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"), + Mockito.eq("NA"), Mockito.eq("NA"), Mockito.eq("failure"), Mockito.eq("imaps://one.two.three.com:993"), Mockito.eq(null), + Mockito.isA(ImapAsyncClientException.class)); + + Assert.assertTrue(future.isDone(), "Future should be done."); + try { + future.get(5, TimeUnit.MILLISECONDS); + Assert.fail("Should throw connect timeout exception"); + } catch (final ExecutionException | InterruptedException ex) { + Assert.assertNotNull(ex, "Expect exception to be thrown."); + Assert.assertNotNull(ex.getCause(), "Expect cause."); + Assert.assertEquals(ex.getClass(), ExecutionException.class, "Class type mismatch."); + final Exception exception = (Exception) ex.getCause(); + Assert.assertEquals(exception.getClass(), ImapAsyncClientException.class, "exception type mismatch." + ex); + Assert.assertNotNull(exception.getCause(), "Cause should not be null"); + Assert.assertEquals(exception.getCause().getClass(), ConnectTimeoutException.class, "Cause should be connection timeout exception"); + Assert.assertSame(exception.getCause(), nettyConnectFuture.cause(), "Cause should be same object"); + Assert.assertEquals(((ImapAsyncClientException) exception).getFailureType(), FailureType.CONNECTION_TIMEOUT_EXCEPTION, + "Exception type should be CONNECTION_TIMEOUT_EXCEPTION"); + } + Mockito.verify(nettyChannel, Mockito.times(1)).isActive(); + Mockito.verify(nettyChannel, Mockito.times(1)).close(); + } + + /** + * Tests createSession method with ConnectTimeout exception. + * + * @throws SSLException will not throw + * @throws URISyntaxException will not throw + * @throws Exception when calling operationComplete() at GenericFutureListener + */ + @Test + public void testCreateSessionConnectionTimeoutFailedChannelIsNull() throws SSLException, URISyntaxException, Exception { + + final Bootstrap bootstrap = Mockito.mock(Bootstrap.class); + final ChannelFuture nettyConnectFuture = Mockito.mock(ChannelFuture.class); + Mockito.when(nettyConnectFuture.isSuccess()).thenReturn(false); + + final ChannelPipeline nettyPipeline = Mockito.mock(ChannelPipeline.class); + + Mockito.when(nettyConnectFuture.cause()).thenReturn(new ConnectTimeoutException("connection timed out")); + Mockito.when(bootstrap.connect(Mockito.anyString(), Mockito.anyInt())).thenReturn(nettyConnectFuture); + + final EventLoopGroup group = Mockito.mock(EventLoopGroup.class); + final Logger logger = Mockito.mock(Logger.class); + Mockito.when(logger.isDebugEnabled()).thenReturn(true); + + final CustomImapAsyncClient aclient = new CustomImapAsyncClient(clock, bootstrap, group, logger); + + final ImapAsyncSessionConfig config = new ImapAsyncSessionConfig(); + config.setConnectionTimeoutMillis(5000); + config.setReadTimeoutMillis(6000); + final List sniNames = null; + + // test create session + final InetSocketAddress localAddress = null; + final URI serverUri = new URI(SERVER_URI_STR); + final Future future = aclient.createSession(serverUri, config, localAddress, sniNames, DebugMode.DEBUG_OFF); + + // verify session creation + Assert.assertNotNull(future, "Future for ImapAsyncSession should not be null."); + + final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(ImapClientChannelInitializer.class); + Mockito.verify(bootstrap, Mockito.times(1)).handler(initializerCaptor.capture()); + Assert.assertEquals(initializerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + final ImapClientChannelInitializer initializer = initializerCaptor.getAllValues().get(0); + + // should not call this connect + Mockito.verify(bootstrap, Mockito.times(0)).connect(Mockito.any(SocketAddress.class), Mockito.any(SocketAddress.class)); + // should call following connect + Mockito.verify(bootstrap, Mockito.times(1)).connect(Mockito.anyString(), Mockito.anyInt()); + final ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(GenericFutureListener.class); + Mockito.verify(nettyConnectFuture, Mockito.times(1)).addListener(listenerCaptor.capture()); + Assert.assertEquals(listenerCaptor.getAllValues().size(), 1, "Unexpected count of ImapClientChannelInitializer."); + + // verify GenericFutureListener.operationComplete() + final GenericFutureListener listener = listenerCaptor.getAllValues().get(0); + listener.operationComplete(nettyConnectFuture); + final ArgumentCaptor handlerCaptorFirst = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addFirst(Mockito.anyString(), handlerCaptorFirst.capture()); + Assert.assertEquals(handlerCaptorFirst.getAllValues().size(), 0, "number of handlers mismatched."); + + final ArgumentCaptor handlerCaptorLast = ArgumentCaptor.forClass(ChannelHandler.class); + Mockito.verify(nettyPipeline, Mockito.times(0)).addLast(Mockito.anyString(), handlerCaptorLast.capture()); + Assert.assertEquals(handlerCaptorLast.getAllValues().size(), 0, "Unexpected count of ChannelHandler added."); + // verify logging messages + Mockito.verify(logger, Mockito.times(1)).error(Mockito.eq("[{},{}] connect operationComplete. result={}, imapServerUri={}, sniNames={}"), + Mockito.eq("NA"), Mockito.eq("NA"), Mockito.eq("failure"), Mockito.eq("imaps://one.two.three.com:993"), Mockito.eq(null), + Mockito.isA(ImapAsyncClientException.class)); + + Assert.assertTrue(future.isDone(), "Future should be done."); + ImapAsyncClientException actual = null; + try { + future.get(5, TimeUnit.MILLISECONDS); + Assert.fail("Should throw connect timeout exception"); + } catch (final ExecutionException | InterruptedException ex) { + Assert.assertNotNull(ex, "Expect exception to be thrown."); + Assert.assertNotNull(ex.getCause(), "Expect cause."); + Assert.assertEquals(ex.getClass(), ExecutionException.class, "Class type mismatch."); + final Exception exception = (Exception) ex.getCause(); + Assert.assertEquals(exception.getClass(), ImapAsyncClientException.class, "exception type mismatch." + ex); + actual = (ImapAsyncClientException) exception; + } + + Assert.assertNotNull(actual.getCause(), "Cause should not be null"); + Assert.assertEquals(actual.getCause().getClass(), ConnectTimeoutException.class, "Cause should be connection timeout exception"); + Assert.assertSame(actual.getCause(), nettyConnectFuture.cause(), "Cause should be same object"); + Assert.assertEquals(actual.getFailureType(), FailureType.CONNECTION_TIMEOUT_EXCEPTION, + "Exception type should be CONNECTION_TIMEOUT_EXCEPTION"); + } + +} \ No newline at end of file