Implement IMAP IDLE

This commit is contained in:
Mario Fetka
2026-07-16 21:02:52 +02:00
parent 07143d740f
commit 9ede4df5be
9 changed files with 214 additions and 2 deletions
+2 -1
View File
@@ -74,7 +74,8 @@ and tested.
app passwords and optional OAUTHBEARER/EXTERNAL. Never advertise mechanisms
that are unavailable or unsafe on the current connection.
- Add POP3 SASL and integrate its authentication with app-password policy.
- Add IMAP IDLE, UIDPLUS, MOVE, SPECIAL-USE, LIST-EXTENDED and LITERAL+.
- IMAP IDLE is implemented with store-event polling and a 29-minute renewal
boundary. Add UIDPLUS, MOVE, SPECIAL-USE, LIST-EXTENDED and LITERAL+.
- Add IMAP ENABLE and UTF8=ACCEPT, then advertise IMAP4rev2 only after every
mandatory rev2 behavior and interoperability test passes.
- Implement SMTPUTF8 end to end through submission, relay, LMTP, queue, DSN,
+10
View File
@@ -1,6 +1,7 @@
add_executable(bongoimap
imapd.c
capabilities.c
idle.c
copy.c
event.c
fetch.c
@@ -33,4 +34,13 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME imap-capabilities COMMAND imap-capabilities-test)
add_executable(imap-idle-test
tests/idle-test.c
idle.c
)
target_include_directories(imap-idle-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME imap-idle COMMAND imap-idle-test)
endif()
+1 -1
View File
@@ -34,7 +34,7 @@ IMAPFormatCapabilities(char *buffer, size_t buffer_size,
buffer[0] = '\0';
if (!Append(buffer, buffer_size, &length,
"* CAPABILITY IMAP4rev1 IMAP4 NAMESPACE XSENDER") ||
"* CAPABILITY IMAP4rev1 IMAP4 NAMESPACE IDLE XSENDER") ||
(!authenticated && tls_active &&
!Append(buffer, buffer_size, &length,
" AUTH=PLAIN AUTH=LOGIN SASL-IR")) ||
+54
View File
@@ -0,0 +1,54 @@
#include "idle.h"
#include <errno.h>
#include <strings.h>
#include <sys/select.h>
#include <time.h>
IMAPIdleResult
IMAPIdleRun(int socket_fd, unsigned int event_interval_seconds,
unsigned int timeout_seconds, IMAPIdleReadLine read_line,
IMAPIdlePollEvents poll_events, void *context)
{
time_t now;
time_t deadline;
time_t next_event_check;
if (socket_fd < 0 || !read_line || !poll_events ||
event_interval_seconds == 0)
return IMAP_IDLE_ERROR;
now = time(NULL);
if (now == (time_t)-1) return IMAP_IDLE_ERROR;
deadline = now + timeout_seconds;
next_event_check = now;
for (;;) {
fd_set readable;
struct timeval timeout;
const char *line = NULL;
int ready;
now = time(NULL);
if (now == (time_t)-1) return IMAP_IDLE_ERROR;
if (now >= deadline) return IMAP_IDLE_TIMEOUT;
if (now >= next_event_check) {
if (!poll_events(context)) return IMAP_IDLE_ERROR;
next_event_check = now + event_interval_seconds;
}
FD_ZERO(&readable);
FD_SET(socket_fd, &readable);
timeout.tv_sec = 1;
timeout.tv_usec = 0;
ready = select(socket_fd + 1, &readable, NULL, NULL, &timeout);
if (ready < 0) {
if (errno == EINTR) continue;
return IMAP_IDLE_ERROR;
}
if (ready == 0) continue;
if (!read_line(context, &line) || !line) return IMAP_IDLE_ERROR;
return strcasecmp(line, "DONE") == 0 ? IMAP_IDLE_DONE
: IMAP_IDLE_INVALID;
}
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef BONGO_IMAP_IDLE_H
#define BONGO_IMAP_IDLE_H
typedef int (*IMAPIdleReadLine)(void *context, const char **line);
typedef int (*IMAPIdlePollEvents)(void *context);
typedef enum {
IMAP_IDLE_ERROR = -1,
IMAP_IDLE_TIMEOUT = -2,
IMAP_IDLE_INVALID = 0,
IMAP_IDLE_DONE = 1
} IMAPIdleResult;
IMAPIdleResult IMAPIdleRun(int socket_fd, unsigned int event_interval_seconds,
unsigned int timeout_seconds,
IMAPIdleReadLine read_line,
IMAPIdlePollEvents poll_events, void *context);
#endif
+56
View File
@@ -44,6 +44,7 @@
#include "imapd.h"
#include "capabilities.h"
#include "idle.h"
ImapGlobal Imap;
@@ -55,6 +56,7 @@ static ProtocolCommand ImapProtocolCommands[] = {
IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CAPABILITY, ImapCommandCapability),
IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_NOOP, ImapCommandNoop),
IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LOGOUT, ImapCommandLogout),
IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_IDLE, ImapCommandIdle),
IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_STARTTLS, ImapCommandStartTls),
IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_AUTHENTICATE, ImapCommandAuthenticate),
IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LOGIN, ImapCommandLogin),
@@ -892,6 +894,60 @@ ImapCommandNoop(void *param)
return(SendError(session->client.conn, session->command.tag, "NOOP", ccode));
}
static int
IdleReadLine(void *opaque, const char **line)
{
ImapSession *session = opaque;
if (ReadCommandLine(session->client.conn, &session->command.buffer,
&session->command.bufferLen) != STATUS_CONTINUE)
return 0;
*line = session->command.buffer;
return 1;
}
static int
IdlePollEvents(void *opaque)
{
ImapSession *session = opaque;
return EventsSend(session, STORE_EVENT_ALL) == STATUS_CONTINUE &&
ConnFlush(session->client.conn) != -1;
}
int
ImapCommandIdle(void *param)
{
ImapSession *session = (ImapSession *)param;
const char *arguments = session->command.buffer + strlen(IMAP_COMMAND_IDLE);
IMAPIdleResult result;
while (IsWhiteSpace(*arguments)) arguments++;
if (*arguments != '\0')
return SendError(session->client.conn, session->command.tag, "IDLE",
STATUS_INVALID_ARGUMENT);
if (session->client.state < STATE_AUTH)
return SendError(session->client.conn, session->command.tag, "IDLE",
STATUS_INVALID_STATE);
if (ConnWrite(session->client.conn, "+ idling\r\n",
sizeof("+ idling\r\n") - 1) == -1 ||
ConnFlush(session->client.conn) == -1)
return STATUS_ABORT;
result = IMAPIdleRun(session->client.conn->socket, 5, 29 * 60,
IdleReadLine, IdlePollEvents, session);
if (result == IMAP_IDLE_DONE) return SendOk(session, "IDLE");
if (result == IMAP_IDLE_INVALID)
return SendError(session->client.conn, session->command.tag, "IDLE",
STATUS_INVALID_ARGUMENT);
if (result == IMAP_IDLE_TIMEOUT) {
ConnWrite(session->client.conn, "* BYE IDLE timeout\r\n",
sizeof("* BYE IDLE timeout\r\n") - 1);
ConnFlush(session->client.conn);
}
return STATUS_ABORT;
}
int
ImapCommandLogout(void *param)
{
+2
View File
@@ -152,6 +152,7 @@ static ImapErrorString ImapErrorStrings[] = {
#define IMAP_COMMAND_CAPABILITY "CAPABILITY"
#define IMAP_COMMAND_NOOP "NOOP"
#define IMAP_COMMAND_LOGOUT "LOGOUT"
#define IMAP_COMMAND_IDLE "IDLE"
/* IMAP session commands - not authenticated state */
#define IMAP_COMMAND_STARTTLS "STARTTLS"
@@ -429,6 +430,7 @@ BOOL CommandSearchInit(void);
int ImapCommandCapability(void *param);
int ImapCommandNoop(void *param);
int ImapCommandLogout(void *param);
int ImapCommandIdle(void *param);
int ImapCommandStartTls(void *param);
int ImapCommandAuthenticate(void *param);
int ImapCommandLogin(void *param);
@@ -13,6 +13,7 @@ main(void)
assert(strstr(buffer, "IMAP4rev1"));
assert(strstr(buffer, " IMAP4 "));
assert(strstr(buffer, "NAMESPACE"));
assert(strstr(buffer, "IDLE"));
assert(strstr(buffer, "XSENDER"));
assert(strstr(buffer, "LOGINDISABLED"));
assert(!strstr(buffer, "STARTTLS"));
+69
View File
@@ -0,0 +1,69 @@
#include "idle.h"
#include <assert.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
typedef struct {
int socket;
int polls;
char line[32];
} TestContext;
static int
ReadLine(void *opaque, const char **line)
{
TestContext *context = opaque;
ssize_t length = read(context->socket, context->line,
sizeof(context->line) - 1);
if (length <= 0) return 0;
while (length > 0 &&
(context->line[length - 1] == '\r' ||
context->line[length - 1] == '\n'))
length--;
context->line[length] = '\0';
*line = context->line;
return 1;
}
static int
PollEvents(void *opaque)
{
TestContext *context = opaque;
context->polls++;
return 1;
}
static void
TestLine(const char *line, IMAPIdleResult expected)
{
int sockets[2];
TestContext context;
assert(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0);
memset(&context, 0, sizeof(context));
context.socket = sockets[0];
assert(write(sockets[1], line, strlen(line)) == (ssize_t)strlen(line));
assert(IMAPIdleRun(sockets[0], 5, 30, ReadLine, PollEvents, &context) ==
expected);
assert(context.polls == 1);
close(sockets[0]);
close(sockets[1]);
}
int
main(void)
{
TestContext context = { -1, 0, "" };
TestLine("DONE\r\n", IMAP_IDLE_DONE);
TestLine("done\r\n", IMAP_IDLE_DONE);
TestLine("NOOP\r\n", IMAP_IDLE_INVALID);
assert(IMAPIdleRun(0, 5, 0, ReadLine, PollEvents, &context) ==
IMAP_IDLE_TIMEOUT);
assert(IMAPIdleRun(-1, 5, 30, ReadLine, PollEvents, &context) ==
IMAP_IDLE_ERROR);
return 0;
}