commits@javamail.java.net

[javamail~mercurial:715] Fix more IdleManager deadlocks related to connection failures - bug 6817

From: <shannon_at_java.net>
Date: Fri, 8 May 2015 23:43:20 +0000

Project: javamail
Repository: mercurial
Revision: 715
Author: shannon
Date: 2015-05-08 23:07:14 UTC
Link:

Log Message:
------------
Clarify that get/setDisposition only operates on the disposition field,
not the entire header.
Fix more IdleManager deadlocks related to connection failures - bug 6817


Revisions:
----------
714
715


Modified Paths:
---------------
mail/src/main/java/javax/mail/internet/MimeBodyPart.java
mail/src/main/java/javax/mail/internet/MimeMessage.java
doc/release/CHANGES.txt
mail/src/main/java/com/sun/mail/iap/Response.java
mail/src/main/java/com/sun/mail/imap/IMAPFolder.java
mail/src/main/java/com/sun/mail/imap/IdleManager.java
mail/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java
mail/src/test/java/com/sun/mail/imap/IMAPIdleManagerTest.java


Diffs:
------
diff -r 9711ace03942 -r 9d2e180e859e mail/src/main/java/javax/mail/internet/MimeBodyPart.java
--- a/mail/src/main/java/javax/mail/internet/MimeBodyPart.java Mon Apr 27 14:34:17 2015 -0700
+++ b/mail/src/main/java/javax/mail/internet/MimeBodyPart.java Fri May 08 15:38:53 2015 -0700
@@ -298,7 +298,7 @@
     }
 
     /**
- * Returns the value of the "Content-Disposition" header field.
+ * Returns the disposition from the "Content-Disposition" header field.
      * This represents the disposition of this part. The disposition
      * describes how the part should be presented to the user. <p>
      *
@@ -316,9 +316,9 @@
     }
 
     /**
- * Set the "Content-Disposition" header field of this body part.
- * If the disposition is null, any existing "Content-Disposition"
- * header field is removed.
+ * Set the disposition in the "Content-Disposition" header field
+ * of this body part. If the disposition is null, any existing
+ * "Content-Disposition" header field is removed.
      *
      * @exception IllegalWriteException if the underlying
      * implementation does not support modification

diff -r 9711ace03942 -r 9d2e180e859e mail/src/main/java/javax/mail/internet/MimeMessage.java
--- a/mail/src/main/java/javax/mail/internet/MimeMessage.java Mon Apr 27 14:34:17 2015 -0700
+++ b/mail/src/main/java/javax/mail/internet/MimeMessage.java Fri May 08 15:38:53 2015 -0700
@@ -1,7 +1,7 @@
 /*
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
  *
- * Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997-2015 Oracle and/or its affiliates. All rights reserved.
  *
  * The contents of this file are subject to the terms of either the GNU
  * General Public License Version 2 only ("GPL") or the Common Development
@@ -1016,7 +1016,7 @@
     }
 
     /**
- * Returns the value of the "Content-Disposition" header field.
+ * Returns the disposition from the "Content-Disposition" header field.
      * This represents the disposition of this part. The disposition
      * describes how the part should be presented to the user. <p>
      *
@@ -1034,9 +1034,9 @@
     }
 
     /**
- * Set the "Content-Disposition" header field of this Message.
- * If <code>disposition</code> is null, any existing "Content-Disposition"
- * header field is removed.
+ * Set the disposition in the "Content-Disposition" header field
+ * of this body part. If the disposition is null, any existing
+ * "Content-Disposition" header field is removed.
      *
      * @exception IllegalWriteException if the underlying
      * implementation does not support modification


diff -r 9d2e180e859e -r 109444121a8a doc/release/CHANGES.txt
--- a/doc/release/CHANGES.txt Fri May 08 15:38:53 2015 -0700
+++ b/doc/release/CHANGES.txt Fri May 08 16:07:14 2015 -0700
@@ -19,6 +19,7 @@
 The following bugs have been fixed in the 1.5.4 release.
 
 K 6804 IdleManager can deadlock with frequent notifications
+K 6817 IdleManager can deadlock when connection fails
 
 
                   CHANGES IN THE 1.5.3 RELEASE

diff -r 9d2e180e859e -r 109444121a8a mail/src/main/java/com/sun/mail/iap/Response.java
--- a/mail/src/main/java/com/sun/mail/iap/Response.java Fri May 08 15:38:53 2015 -0700
+++ b/mail/src/main/java/com/sun/mail/iap/Response.java Fri May 08 16:07:14 2015 -0700
@@ -1,7 +1,7 @@
 /*
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
  *
- * Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997-2015 Oracle and/or its affiliates. All rights reserved.
  *
  * The contents of this file are subject to the terms of either the GNU
  * General Public License Version 2 only ("GPL") or the Common Development
@@ -58,6 +58,8 @@
     protected byte[] buffer = null;
     protected int type = 0;
     protected String tag = null;
+ /** @since JavaMail 1.5.4 */
+ protected Exception ex;
 
     private static final int increment = 100;
 
@@ -133,6 +135,7 @@
         err = err.replace('\r', ' ').replace('\n', ' ');
         Response r = new Response(err);
         r.type |= SYNTHETIC;
+ r.ex = ex;
         return r;
     }
 
@@ -525,6 +528,15 @@
     }
 
     /**
+ * Return the exception for a synthetic BYE response.
+ *
+ * @since JavaMail 1.5.4
+ */
+ public Exception getException() {
+ return ex;
+ }
+
+ /**
      * Reset pointer to beginning of response.
      */
     public void reset() {

diff -r 9d2e180e859e -r 109444121a8a mail/src/main/java/com/sun/mail/imap/IMAPFolder.java
--- a/mail/src/main/java/com/sun/mail/imap/IMAPFolder.java Fri May 08 15:38:53 2015 -0700
+++ b/mail/src/main/java/com/sun/mail/imap/IMAPFolder.java Fri May 08 16:07:14 2015 -0700
@@ -50,6 +50,7 @@
 import java.util.Locale;
 import java.util.logging.Level;
 import java.io.*;
+import java.net.SocketTimeoutException;
 import java.nio.channels.SocketChannel;
 
 import javax.mail.*;
@@ -2965,10 +2966,44 @@
      * @since JavaMail 1.5.2
      */
     boolean handleIdle(boolean once) throws MessagingException {
+ Response r = null;
         do {
- Response r = protocol.readIdleResponse();
+ r = protocol.readIdleResponse();
             try {
                 synchronized (messageCacheLock) {
+ if (r.isBYE() && r.isSynthetic() && idleState == IDLE) {
+ /*
+ * If it was a timeout and no bytes were transferred
+ * we ignore it and go back and read again.
+ * If the I/O was otherwise interrupted, and no
+ * bytes were transferred, we take it as a request
+ * to abort the IDLE.
+ */
+ Exception ex = r.getException();
+ if (ex instanceof InterruptedIOException &&
+ ((InterruptedIOException)ex).
+ bytesTransferred == 0) {
+ if (ex instanceof SocketTimeoutException) {
+ logger.finest(
+ "handleIdle: ignoring socket timeout");
+ r = null; // repeat do/while loop
+ } else {
+ logger.finest("handleIdle: interrupting IDLE");
+ IdleManager im = idleManager;
+ if (im != null) {
+ logger.finest(
+ "handleIdle: request IdleManager to abort");
+ im.requestAbort(this);
+ } else {
+ logger.finest("handleIdle: abort IDLE");
+ protocol.idleAbort();
+ idleState = ABORTING;
+ }
+ // normally will exit the do/while loop
+ }
+ continue;
+ }
+ }
                     boolean done = true;
                     try {
                         if (r == null || protocol == null ||
@@ -2997,13 +3032,13 @@
                     }
                 }
             } catch (ConnectionException cex) {
- // Oops, the store or folder died on us.
- throwClosedException(cex);
+ // Oops, the folder died on us.
+ throw new FolderClosedException(this, cex.getMessage());
             } catch (ProtocolException pex) {
                 throw new MessagingException(pex.getMessage(), pex);
             }
         // keep processing responses already in our buffer
- } while (protocol.hasResponse());
+ } while (r == null || protocol.hasResponse());
         return true;
     }
 
@@ -3079,7 +3114,9 @@
                     }
                 } catch (Exception ex) {
                     // assume it's a connection failure; nothing more to do
+ logger.log(Level.FINEST, "Exception in idleAbortWait", ex);
                 }
+ logger.finest("IDLE aborted");
             }
         }
     }
@@ -3410,6 +3447,11 @@
     protected IMAPProtocol getProtocol() throws ProtocolException {
         assert Thread.holdsLock(messageCacheLock);
         waitIfIdle();
+ // if we no longer have a protocol object after waiting, it probably
+ // means the connection has been closed due to a communnication error,
+ // or possibly because the folder has been closed
+ if (protocol == null)
+ throw new ConnectionException("Connection closed");
         return protocol;
     }
 

diff -r 9d2e180e859e -r 109444121a8a mail/src/main/java/com/sun/mail/imap/IdleManager.java
--- a/mail/src/main/java/com/sun/mail/imap/IdleManager.java Fri May 08 15:38:53 2015 -0700
+++ b/mail/src/main/java/com/sun/mail/imap/IdleManager.java Fri May 08 16:07:14 2015 -0700
@@ -1,7 +1,7 @@
 /*
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
  *
- * Copyright (c) 2014 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014-2015 Oracle and/or its affiliates. All rights reserved.
  *
  * The contents of this file are subject to the terms of either the GNU
  * General Public License Version 2 only ("GPL") or the Common Development
@@ -42,6 +42,7 @@
 
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.net.Socket;
 import java.nio.*;
 import java.nio.channels.*;
 import java.util.*;
@@ -130,6 +131,7 @@
  * @since JavaMail 1.5.2
  */
 public class IdleManager {
+ private Executor es;
     private Selector selector;
     private MailLogger logger;
     private volatile boolean die = false;
@@ -146,6 +148,7 @@
      * @exception IOException for Selector failures
      */
     public IdleManager(Session session, Executor es) throws IOException {
+ this.es = es;
         logger = new MailLogger(this.getClass(), "DEBUG IMAP", session);
         selector = Selector.open();
         es.execute(new Runnable() {
@@ -193,7 +196,7 @@
      * blocking I/O mode when not selecting, so wake up the selector,
      * which will process this request when it wakes up.
      */
- synchronized void requestAbort(IMAPFolder folder) {
+ void requestAbort(IMAPFolder folder) {
         toAbort.add(folder);
         selector.wakeup();
     }
@@ -283,34 +286,25 @@
      */
     private boolean processKeys() throws IOException {
         boolean more = false;
- /*
- * First, process any folders that we need to abort.
- */
         IMAPFolder folder;
- while ((folder = toAbort.poll()) != null) {
- logger.log(Level.FINE,
- "IdleManager aborting IDLE for folder: {0}", folder);
- SocketChannel sc = folder.getChannel();
- if (sc == null)
- continue;
- SelectionKey sk = sc.keyFor(selector);
- // have to cancel so we can switch back to blocking I/O mode
- if (sk != null)
- sk.cancel();
- // switch back to blocking to allow normal I/O
- sc.configureBlocking(true);
- folder.idleAbort(); // send the DONE message
- // watch for OK response to DONE
- toWatch.add(folder);
- more = true;
- }
 
         /*
- * Now, process any channels with data to read.
+ * First, process any channels with data to read.
          */
         Set<SelectionKey> selectedKeys = selector.selectedKeys();
+ /*
+ * XXX - this is simpler, but it can fail with
+ * ConncurentModificationException
+ *
         for (SelectionKey sk : selectedKeys) {
             selectedKeys.remove(sk); // only process each key once
+ ...
+ }
+ */
+ Iterator<SelectionKey> it = selectedKeys.iterator();
+ while (it.hasNext()) {
+ SelectionKey sk = it.next();
+ it.remove(); // only process each key once
             // have to cancel so we can switch back to blocking I/O mode
             sk.cancel();
             folder = (IMAPFolder)sk.attachment();
@@ -339,6 +333,44 @@
                     ex);
             }
         }
+
+ /*
+ * Now, process any folders that we need to abort.
+ */
+ while ((folder = toAbort.poll()) != null) {
+ logger.log(Level.FINE,
+ "IdleManager aborting IDLE for folder: {0}", folder);
+ SocketChannel sc = folder.getChannel();
+ if (sc == null)
+ continue;
+ SelectionKey sk = sc.keyFor(selector);
+ // have to cancel so we can switch back to blocking I/O mode
+ if (sk != null)
+ sk.cancel();
+ // switch back to blocking to allow normal I/O
+ sc.configureBlocking(true);
+
+ // if there's a read timeout, have to do the abort in a new thread
+ Socket sock = sc.socket();
+ if (sock != null && sock.getSoTimeout() > 0) {
+ logger.fine("IdleManager requesting DONE with timeout");
+ toWatch.remove(folder);
+ final IMAPFolder folder0 = folder;
+ es.execute(new Runnable() {
+ @Override
+ public void run() {
+ // send the DONE and wait for the response
+ folder0.idleAbortWait();
+ }
+ });
+ } else {
+ folder.idleAbort(); // send the DONE message
+ // watch for OK response to DONE
+ toWatch.add(folder);
+ more = true;
+ }
+ }
+
         return more;
     }
 

diff -r 9d2e180e859e -r 109444121a8a mail/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java
--- a/mail/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java Fri May 08 15:38:53 2015 -0700
+++ b/mail/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java Fri May 08 16:07:14 2015 -0700
@@ -284,8 +284,10 @@
         if (ir.keyEquals("PREAUTH")) {
             authenticated = true;
             setCapabilities(r);
- } else
+ } else {
+ disconnect();
             throw new ConnectionException(this, r);
+ }
     }
 
     /**
@@ -2634,28 +2636,14 @@
         if (idleTag == null)
             return null; // IDLE not in progress
         Response r = null;
- while (r == null) {
- try {
- r = readResponse();
- } catch (InterruptedIOException iioex) {
- /*
- * If a socket timeout was set, the read will timeout
- * before the IDLE times out. In that case, just go
- * back and read some more. After all, the point of
- * IDLE is to sit here and wait until something happens.
- */
- if (iioex.bytesTransferred == 0)
- r = null; // keep trying
- else
- // convert this into a BYE response
- r = Response.byeResponse(iioex);
- } catch (IOException ioex) {
- // convert this into a BYE response
- r = Response.byeResponse(ioex);
- } catch (ProtocolException pex) {
- // convert this into a BYE response
- r = Response.byeResponse(pex);
- }
+ try {
+ r = readResponse();
+ } catch (IOException ioex) {
+ // convert this into a BYE response
+ r = Response.byeResponse(ioex);
+ } catch (ProtocolException pex) {
+ // convert this into a BYE response
+ r = Response.byeResponse(pex);
         }
         return r;
     }

diff -r 9d2e180e859e -r 109444121a8a mail/src/test/java/com/sun/mail/imap/IMAPIdleManagerTest.java
--- a/mail/src/test/java/com/sun/mail/imap/IMAPIdleManagerTest.java Fri May 08 15:38:53 2015 -0700
+++ b/mail/src/test/java/com/sun/mail/imap/IMAPIdleManagerTest.java Fri May 08 16:07:14 2015 -0700
@@ -45,11 +45,15 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 import javax.mail.Session;
 import javax.mail.Store;
 import javax.mail.Folder;
 import javax.mail.FetchProfile;
+import javax.mail.MessagingException;
+import javax.mail.event.ConnectionAdapter;
+import javax.mail.event.ConnectionEvent;
 
 import com.sun.mail.test.TestServer;
 
@@ -58,29 +62,35 @@
 import org.junit.rules.Timeout;
 import static org.junit.Assert.fail;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 /**
- * Test that IdleManager handles multiple responses in a single
- * network packet correctly.
+ * Test IdleManager.
  */
 public final class IMAPIdleManagerTest {
 
+ private static final int TIMEOUT = 1000; // 1 second
+
     // timeout the test in case of deadlock
     @Rule
- public Timeout deadlockTimeout = new Timeout(5000);
+ public Timeout deadlockTimeout = new Timeout(5 * TIMEOUT);
 
+ /**
+ * Test that IdleManager handles multiple responses in a single packet.
+ */
     @Test
     public void testDone() {
- test(new IMAPHandlerIdleDone());
+ testSuccess(new IMAPHandlerIdleDone());
     }
 
     @Test
     public void testExists() {
- test(new IMAPHandlerIdleExists());
+ testSuccess(new IMAPHandlerIdleExists());
     }
 
- private void test(IMAPHandlerIdle handler) {
+ private void testSuccess(IMAPHandlerIdle handler) {
         TestServer server = null;
+ IdleManager idleManager = null;
         try {
             server = new TestServer(handler);
             server.start();
@@ -93,7 +103,7 @@
             //session.setDebug(true);
 
             ExecutorService executor = Executors.newCachedThreadPool();
- IdleManager idleManager = new IdleManager(session, executor);
+ idleManager = new IdleManager(session, executor);
 
             final IMAPStore store = (IMAPStore)session.getStore("imap");
             try {
@@ -124,6 +134,8 @@
             e.printStackTrace();
             fail(e.getMessage());
         } finally {
+ if (idleManager != null)
+ idleManager.stop();
             if (server != null) {
                 server.quit();
             }
@@ -131,22 +143,167 @@
     }
 
     /**
- * Custom handler.
+ * Test that IdleManager handles timeouts.
      */
- private static class IMAPHandlerIdle extends IMAPHandler {
- // must be static because handler is cloned for each connection
- protected static CountDownLatch latch = new CountDownLatch(1);
+ @Test
+ public void testBeforeIdleTimeout() {
+ testFailure(new IMAPHandlerBeforeIdleTimeout(), true);
+ }
 
+ @Test
+ public void testIdleTimeout() {
+ testFailure(new IMAPHandlerIdleTimeout(), true);
+ }
+
+ @Test
+ public void testDoneTimeout() {
+ testFailure(new IMAPHandlerDoneTimeout(), true);
+ }
+
+ /**
+ * Test that IdleManager handles connection failures.
+ */
+ @Test
+ public void testBeforeIdleDrop() {
+ testFailure(new IMAPHandlerBeforeIdleDrop(), false);
+ }
+
+ @Test
+ public void testIdleDrop() {
+ testFailure(new IMAPHandlerIdleDrop(), false);
+ }
+
+ @Test
+ public void testDoneDrop() {
+ testFailure(new IMAPHandlerDoneDrop(), false);
+ }
+
+ private void testFailure(IMAPHandlerIdle handler, boolean setTimeout) {
+ TestServer server = null;
+ IdleManager idleManager = null;
+ final CountDownLatch closedLatch = new CountDownLatch(1);
+ try {
+ server = new TestServer(handler);
+ server.start();
+
+ final Properties properties = new Properties();
+ properties.setProperty("mail.imap.host", "localhost");
+ properties.setProperty("mail.imap.port", "" + server.getPort());
+ if (setTimeout)
+ properties.setProperty("mail.imap.timeout", "" + TIMEOUT);
+ properties.setProperty("mail.imap.usesocketchannels", "true");
+ final Session session = Session.getInstance(properties);
+ //session.setDebug(true);
+
+ ExecutorService executor = Executors.newCachedThreadPool();
+ idleManager = new IdleManager(session, executor);
+
+ final IMAPStore store = (IMAPStore)session.getStore("imap");
+ try {
+ store.connect("test", "test");
+ Folder folder = store.getFolder("INBOX");
+ folder.open(Folder.READ_WRITE);
+ idleManager.watch(folder);
+ handler.waitForIdle();
+
+ // now do something that is sure to touch the server
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.ENVELOPE);
+ folder.fetch(folder.getMessages(), fp);
+
+ fail("No exception");
+ } catch (MessagingException mex) {
+ // success!
+ } catch (Exception ex) {
+ System.out.println(ex);
+ //ex.printStackTrace();
+ fail(ex.toString());
+ } finally {
+ store.close();
+ }
+ } catch (final Exception e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ } finally {
+ if (idleManager != null)
+ idleManager.stop();
+ if (server != null) {
+ server.quit();
+ }
+ }
+ }
+
+ /**
+ * Base class for custom handler.
+ */
+ private static abstract class IMAPHandlerIdle extends IMAPHandler {
         @Override
         public void select() throws IOException {
             numberOfMessages = 1;
             super.select();
         }
 
+ public abstract void waitForIdle() throws InterruptedException;
+ }
+
+ /**
+ * Custom handler. Respond to DONE with a single packet containing
+ * EXISTS and OK.
+ */
+ private static final class IMAPHandlerIdleDone extends IMAPHandlerIdle {
+ // must be static because handler is cloned for each connection
+ private static CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
         public void idle() throws IOException {
             cont();
             latch.countDown();
             idleWait();
+ println("* 3 EXISTS\r\n" + tag + " OK");
+ }
+
+ public void waitForIdle() throws InterruptedException {
+ latch.await();
+ }
+ }
+
+ /**
+ * Custom handler. Send two EXISTS responses in a single packet.
+ */
+ private static final class IMAPHandlerIdleExists extends IMAPHandlerIdle {
+ // must be static because handler is cloned for each connection
+ private static CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void idle() throws IOException {
+ cont();
+ latch.countDown();
+ idleWait();
+ println("* 2 EXISTS\r\n* 3 EXISTS");
+ ok();
+ }
+
+ public void waitForIdle() throws InterruptedException {
+ latch.await();
+ }
+ }
+
+ /**
+ * Custom handler. Delay long enough before IDLE starts to force a timeout.
+ */
+ private static final class IMAPHandlerBeforeIdleTimeout
+ extends IMAPHandlerIdle {
+ // must be static because handler is cloned for each connection
+ private static CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void idle() throws IOException {
+ try {
+ Thread.sleep(2 * TIMEOUT);
+ } catch (InterruptedException ex) { }
+ cont();
+ latch.countDown();
+ idleWait();
             ok();
         }
 
@@ -156,30 +313,107 @@
     }
 
     /**
- * Custom handler. Respond to DONE with a single packet containing
- * EXISTS and OK.
+ * Custom handler. Delay long enough after IDLE starts to force a timeout.
      */
- private static final class IMAPHandlerIdleDone extends IMAPHandlerIdle {
+ private static final class IMAPHandlerIdleTimeout extends IMAPHandlerIdle {
+ // must be static because handler is cloned for each connection
+ private static CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void idle() throws IOException {
+ cont();
+ latch.countDown();
+ try {
+ Thread.sleep(2 * TIMEOUT);
+ } catch (InterruptedException ex) { }
+ idleWait();
+ ok();
+ }
+
+ public void waitForIdle() throws InterruptedException {
+ latch.await();
+ }
+ }
+
+ /**
+ * Custom handler. Delay long enough after DONE received to force a
+ * timeout.
+ */
+ private static final class IMAPHandlerDoneTimeout extends IMAPHandlerIdle {
+ // must be static because handler is cloned for each connection
+ private static CountDownLatch latch = new CountDownLatch(1);
+
         @Override
         public void idle() throws IOException {
             cont();
             latch.countDown();
             idleWait();
- println("* 3 EXISTS\r\n" + tag + " OK");
+ try {
+ Thread.sleep(2 * TIMEOUT);
+ } catch (InterruptedException ex) { }
+ ok();
         }
+
+ public void waitForIdle() throws InterruptedException {
+ latch.await();
+ }
     }
 
     /**
- * Custom handler. Send two EXISTS responses in a single packet.
+ * Custom handler. Drop the connection before IDLE started.
      */
- private static final class IMAPHandlerIdleExists extends IMAPHandlerIdle {
+ private static final class IMAPHandlerBeforeIdleDrop extends
+ IMAPHandlerIdle {
+ // must be static because handler is cloned for each connection
+ private static CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void idle() throws IOException {
+ latch.countDown();
+ exit();
+ }
+
+ public void waitForIdle() throws InterruptedException {
+ latch.await();
+ }
+ }
+
+ /**
+ * Custom handler. Drop the connection after IDLE started.
+ */
+ private static final class IMAPHandlerIdleDrop extends IMAPHandlerIdle {
+ // must be static because handler is cloned for each connection
+ private static CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void idle() throws IOException {
+ cont();
+ latch.countDown();
+ exit();
+ }
+
+ public void waitForIdle() throws InterruptedException {
+ latch.await();
+ }
+ }
+
+ /**
+ * Custom handler. Drop the connection after DONE received.
+ */
+ private static final class IMAPHandlerDoneDrop extends IMAPHandlerIdle {
+ // must be static because handler is cloned for each connection
+ private static CountDownLatch latch = new CountDownLatch(1);
+
         @Override
         public void idle() throws IOException {
             cont();
             latch.countDown();
             idleWait();
- println("* 2 EXISTS\r\n* 3 EXISTS");
- ok();
+ exit();
         }
+
+ public void waitForIdle() throws InterruptedException {
+ latch.await();
+ }
     }
 }