Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package org.eclipse.angus.mail.imap;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import jakarta.mail.Folder;
import jakarta.mail.Session;
import jakarta.mail.Store;

import org.eclipse.angus.mail.test.SingleThreadServer;
import org.junit.Test;

public class IdleTimeoutTest {

private static final long TIMEOUT = 3000;
private final Executor executor = Executors.newCachedThreadPool();

@Test
public void testTimeout() throws Exception {
try (SingleThreadServer server = new SingleThreadServer()) {

// Predefine the server responses, so it is mocking an IMAP server

server.prepareResponse("A0 CAPABILITY\r\n",
"* CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN AUTH=LOGIN IDLE UIDPLUS\r\n" +
"A0 OK CAPABILITY completed\r\n");

server.prepareResponse("A1 AUTHENTICATE PLAIN\r\n", "+\r\n");
server.prepareResponse("AHVzZXJAdGVzdC5jb20AMTIzNA==\r\n",
"A1 OK AUTHENTICATE completed\r\n");

server.prepareResponse("A2 CAPABILITY\r\n",
"* CAPABILITY IMAP4rev1 IDLE UIDPLUS\r\n" +
"A2 OK CAPABILITY completed\r\n");

server.prepareResponse("A3 SELECT INBOX\r\n",
"* 0 EXISTS\r\n" +
"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n" +
"A3 OK [READ-WRITE] SELECT completed\r\n");

server.prepareResponse("A4 IDLE\r\n", "+ idling\r\n");

server.start();
Properties properties = new Properties();
properties.setProperty("mail.imap.host", "localhost");
properties.setProperty("mail.imap.port", Integer.toString(server.getPort()));
// We want to verify what happens when client gets SocketTimeoutException
properties.setProperty("mail.imap.timeout", Long.toString(TIMEOUT));
// Required to IDLE
properties.setProperty("mail.imap.usesocketchannels", "true");
Session session = Session.getDefaultInstance(properties);
Store store = session.getStore("imap");
store.connect("user@test.com", "1234");
IdleManager im = new IdleManager(session, executor);
Folder folder = store.getFolder("INBOX");
folder.open(Folder.READ_WRITE);

im.watch(folder);
// Client is going to get the SocketTimeoutException after TIMEOUT
Thread.sleep(9999999L);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package org.eclipse.angus.mail.test;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadServer implements AutoCloseable {

private int port;
private ServerSocket server;
private Socket serverSocket;
private ExecutorService executor;
private volatile boolean running;
private Map<String, String> predefinedResponses = new HashMap<>();

public void start() {
try {
executor = Executors.newSingleThreadExecutor();
server = new ServerSocket(0);
port = server.getLocalPort();
executor.submit(() -> {
try {
serverSocket = server.accept();
System.out.println("Connected to " + serverSocket.getRemoteSocketAddress());
System.out.println("Server > * OK IMAP4rev1 Service Ready\r\n");
writeInServer("* OK IMAP4rev1 Service Ready\r\n");
running = true;
byte[] buffer = new byte[1024];
int bytesRead;
while (running && (bytesRead = serverSocket.getInputStream().read(buffer)) != -1) {
String data = new String(buffer, 0, bytesRead);
System.out.println("Client > " + data);
String response = predefinedResponses.get(data);
if (response != null) {
System.out.println("Server > " + response);
writeInServer(response);
}
}
} catch (IOException e1) {
throw new UncheckedIOException(e1);
}
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

public void prepareResponse(String clientSends, String serverResponds) {
predefinedResponses.put(clientSends, serverResponds);
}

public void stop() {
if (running) {
running = false;
close(serverSocket);
serverSocket = null;
close(server);
server = null;
if (executor != null) {
executor.shutdown();
executor = null;
}
}
}

private void close(Closeable c) {
if (c != null) {
try {
c.close();
} catch (IOException e) {}
}
}

private void writeInServer(String data) {
try {
OutputStream out = serverSocket.getOutputStream();
out.write(data.getBytes());
out.flush();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

public int getPort() {
return port;
}

@Override
public void close() throws Exception {
stop();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
Expand Down Expand Up @@ -3262,7 +3262,9 @@ boolean handleIdle(boolean once) throws MessagingException {
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.
* we abort IDLE and request IDLE again,
* so the server can continue pushing.
*
* If the I/O was otherwise interrupted, and no
* bytes were transferred, we take it as a request
* to abort the IDLE.
Expand All @@ -3272,9 +3274,13 @@ boolean handleIdle(boolean once) throws MessagingException {
((InterruptedIOException) ex).
bytesTransferred == 0) {
if (ex instanceof SocketTimeoutException) {
logger.finest(
"handleIdle: ignoring socket timeout");
r = null; // repeat do/while loop
logger.finest("handleIdle: socket timeout");
// Next aborts and read responses
idleAbortWait();
// Reissue the IDLE
if (startIdle(idleManager)) {
r = null; // repeat do/while loop
}
} else {
logger.finest("handleIdle: interrupting IDLE");
IdleManager im = idleManager;
Expand Down