Implement IMAP IDLE
This commit is contained in:
+2
-1
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")) ||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user