Skip to content

Commit 24e4b76

Browse files
committed
net_imap: Improve robustness of remote IMAP client recreation.
Proxied IMAP clients are not always recreated and restored to the same state that they were in originally. This manifests itself as receiving "BAD Command received in Invalid state" or potentially "Must select a mailbox first" when attempting to FETCH a message on a remote IMAP server where the currently selected mailbox has been idle for some time. To fix this: * Restore the selected folder of the foreground client, so whatever mailbox was selected originally is reselected when recreated. The SELECT happens in the background, transparently. * Fix detection of idling client when attempting to send command. This should never be okay so always trigger a soft assertion.
1 parent eed0f9f commit 24e4b76

File tree

4 files changed

+104
-39
lines changed

4 files changed

+104
-39
lines changed

nets/net_imap.c

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,8 @@ static int generate_mailbox_name(struct imap_session *imap, const char *restrict
410410
fulldir++;
411411
}
412412

413-
if (strlen_zero(fulldir)) {
413+
if (bbs_assertion_failed(!strlen_zero(fulldir))) {
414414
bbs_error("Empty maildir? (%s)\n", maildir);
415-
bbs_soft_assert(0);
416415
return -1;
417416
}
418417

@@ -1434,7 +1433,7 @@ static int handle_select(struct imap_session *imap, char *s, enum select_type re
14341433
}
14351434
if (imap->client) {
14361435
int modseq = 0;
1437-
char *remotename = remote_mailbox_name(imap->client, mailbox);
1436+
const char *remotename = remote_mailbox_name(imap->client, mailbox);
14381437
REPLACE(imap->folder, mailbox);
14391438
/* Reconstruct the SELECT, fortunately this is not too bad */
14401439
return imap_client_send_wait_response_cb(imap->client, -1, 5000, remote_select_cb, &modseq, "%s \"%s\"\r\n", readonly == CMD_SELECT ? "SELECT" : "EXAMINE", remotename);
@@ -1530,7 +1529,7 @@ static int handle_status(struct imap_session *imap, char *s)
15301529
return 0;
15311530
}
15321531
if (traversal.client) {
1533-
char *remotename = remote_mailbox_name(traversal.client, mailbox);
1532+
const char *remotename = remote_mailbox_name(traversal.client, mailbox);
15341533
int wantsize = strstr(s, "SIZE") ? 1 : 0;
15351534
/* If the client wants SIZE but the remote server doesn't support it, we'll have to fake it by translating */
15361535
if (wantsize && !(traversal.client->virtcapabilities & IMAP_CAPABILITY_STATUS_SIZE)) {
@@ -4007,7 +4006,15 @@ static int test_remote_mailbox_substitution(void)
40074006
static int idle_stop(struct imap_session *imap)
40084007
{
40094008
imap->idle = 0;
4010-
/* IDLE for virtual mailboxes (proxied) is handled in the IDLE command itself */
4009+
/* IDLE for virtual mailboxes (proxied) is handled in the IDLE command itself,
4010+
* so it shouldn't be idling at this point. */
4011+
if (bbs_assertion_failed(!imap->client || !imap->client->idling)) {
4012+
/* We're not supposed to be idling, but if we are somehow, stop,
4013+
* or successive commands will fail. */
4014+
if (imap_client_idle_stop(imap->client)) {
4015+
return -1;
4016+
}
4017+
}
40114018
_imap_reply(imap, "%s OK IDLE terminated\r\n", imap->savedtag); /* Use tag from IDLE request */
40124019
free_if(imap->savedtag);
40134020
return 0;
@@ -4207,6 +4214,9 @@ static int handle_idle(struct imap_session *imap)
42074214

42084215
if (!client) {
42094216
if (imap->client) {
4217+
/* The client wants to do something with the foreground remote client connection.
4218+
* We probably just received a "DONE" from the client,
4219+
* so go ahead and terminate idling before we return. */
42104220
imap_client_integrity_check(imap, imap->client);
42114221
/* Stop idling on the selected mailbox (remote) */
42124222
if (imap_client_idle_stop(imap->client)) {
@@ -4231,10 +4241,15 @@ static int handle_idle(struct imap_session *imap)
42314241
* This here is the case where our local user did something,
42324242
* the case of getting activity on a particular client,
42334243
* and handling its disconnect in realtime, is handled below. */
4244+
4245+
/* Because we were trying to stop IDLE, ensure the client isn't recreated to idling state, since we want to use it.
4246+
* imap_recreate_client will enable IDLE if it was previously set, so manually unset it first. */
4247+
imap->client->idling = 0;
42344248
imap_recreate_client(imap, imap->client);
4249+
imap_clients_renew_idle(imap, imap->client); /* No need to renew IDLE on the foreground client, we just recreated it */
42354250
}
42364251
}
4237-
imap_clients_renew_idle(imap); /* In case some of them are close to expiring, renew them now before returning */
4252+
imap_clients_renew_idle(imap, NULL); /* In case some of them are close to expiring, renew them now before returning */
42384253
break; /* Client terminated the idle. Stop idling and return to read the next command. */
42394254
}
42404255
/* For remote NOTIFY: Some remote IMAP server sent us something.
@@ -4322,7 +4337,7 @@ static int handle_idle(struct imap_session *imap)
43224337
}
43234338
}
43244339
}
4325-
imap_clients_renew_idle(imap);
4340+
imap_clients_renew_idle(imap, NULL);
43264341
} else {
43274342
/* Nothing yet. Send an "IDLE ping" to check in... */
43284343
idleleft -= pollms;
@@ -4343,7 +4358,7 @@ static int handle_idle(struct imap_session *imap)
43434358
imap_send(imap, "OK Still here");
43444359
lastactivity = now;
43454360
}
4346-
imap_clients_renew_idle(imap);
4361+
imap_clients_renew_idle(imap, NULL);
43474362
}
43484363
}
43494364
}
@@ -4397,8 +4412,9 @@ static int imap_process(struct imap_session *imap, char *s)
43974412
imap->finalized_response = 0; /* New command, reset */
43984413

43994414
if (imap->idle || (imap->alerted == 1 && !strcasecmp(s, "DONE"))) {
4400-
/* Thunderbird clients will still send "DONE" if we send a tagged reply during the IDLE,
4401-
* but Microsoft Outlook will not, so handle both cases, i.e. tolerate the redundant DONE. */
4415+
/* Mozilla clients will still send "DONE" if we send a tagged reply during the IDLE,
4416+
* but Microsoft Outlook will not, so handle both cases, i.e. tolerate the redundant DONE (2nd condition). */
4417+
bbs_debug(5, "Received %sIDLE termination\n", imap->idle ? "" : "redundant ");
44024418
return idle_stop(imap);
44034419
} else if (imap->alerted == 1) {
44044420
imap->alerted = 2;

nets/net_imap/imap_client.c

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
#include "nets/net_imap/imap.h"
3232
#include "nets/net_imap/imap_client.h"
3333

34+
/* Per the RFC, don't idle for more than 30 minutes at a time */
35+
#define MAX_IDLE_SECS 1800
36+
3437
extern unsigned int maxuserproxies;
3538

3639
/* Check client pointers for integrity before/after various operations.
@@ -204,6 +207,11 @@ int imap_poll(struct imap_session *imap, int ms, struct imap_client **clientout)
204207

205208
int imap_client_idle_start(struct imap_client *client)
206209
{
210+
bbs_debug(5, "Starting IDLE for client %s\n", client->name);
211+
if (bbs_assertion_failed(!client->idling)) {
212+
bbs_warning("Client %s already idling\n", client->name);
213+
return -1;
214+
}
207215
/* Now, IDLE on it so we get updates for that mailbox */
208216
if (SWRITE(client->client.wfd, "idle IDLE\r\n") < 0) {
209217
return -1;
@@ -220,6 +228,11 @@ int imap_client_idle_start(struct imap_client *client)
220228

221229
int imap_client_idle_stop(struct imap_client *client)
222230
{
231+
bbs_debug(5, "Stopping IDLE for client %s\n", client->name);
232+
if (bbs_assertion_failed(client->idling)) {
233+
bbs_warning("Client %s not currently idling\n", client->name);
234+
return -1;
235+
}
223236
if (SWRITE(client->client.wfd, "DONE\r\n") < 0) {
224237
bbs_error("Failed to write to idling client '%s', must be dead\n", client->name);
225238
return -1;
@@ -283,6 +296,8 @@ static struct imap_client *create_client_after_purge(struct imap_session *imap,
283296
* in which case, this will fail. In that case, just let it go. */
284297
if (exists) {
285298
bbs_warning("Failed to recreate %s client %s\n", foreground ? "foreground" : "background", name);
299+
} else {
300+
bbs_debug(3, "Client '%s' does not exist any more in configuration\n", name);
286301
}
287302
return NULL;
288303
}
@@ -299,24 +314,47 @@ int imap_recreate_client(struct imap_session *imap, struct imap_client *client)
299314
int was_idling = client->idling;
300315
int foreground = imap->client == client;
301316
char name[256];
317+
char mb_name[256];
302318

303319
safe_strncpy(name, client->name, sizeof(name)); /* Store name before destroying the client */
304320

321+
/* If a particular mailbox is selected currently, we need to re-SELECT it after recreation */
322+
if (foreground) {
323+
const char *remotename = remote_mailbox_name(imap->client, imap->folder);
324+
safe_strncpy(mb_name, remotename, sizeof(remotename));
325+
}
326+
305327
/* Destroy old client before creating it again. */
306328
if (foreground) {
307329
imap->client = NULL;
308330
}
309331
imap_client_unlink(imap, client); /* returns void, so can't check success */
332+
client = NULL;
310333

311334
/* Now, create it again. */
312-
newclient = create_client_after_purge(imap, name, foreground, was_idling);
313-
if (newclient && foreground) {
335+
newclient = create_client_after_purge(imap, name, foreground, 0); /* Don't resume idling just yet, if we need to SELECT. Do so at the end. */
336+
if (!newclient) {
337+
bbs_warning("Failed to recreate client %s\n", name);
338+
return -1;
339+
}
340+
if (foreground) {
341+
/* Reselect the mailbox that was created.
342+
* Since the client is being recreated transparently, the actual local client may go ahead
343+
* and perform a mailbox operation (e.g. FETCH), so we need to make sure the server is in the
344+
* same state after the recreation as it was before. */
345+
bbs_debug(5, "Reselecting previously selected mailbox '%s'\n", mb_name);
346+
/* Do not pass anything through from the remote server, since this is supposed to completely transparent.
347+
* None of the untagged SELECT responses should pass through, prior to the tagged IDLE response. */
348+
imap_client_send_wait_response_noecho(newclient, -1, 5000, "%s \"%s\"\r\n", "SELECT", mb_name);
314349
imap->client = newclient; /* At this point, the new client has been swapped in, and the rest of the module is none the wiser. */
315350
}
316-
return newclient ? 0 : -1;
351+
if (was_idling) {
352+
imap_client_idle_start(newclient); /* Resume idling, if that's what it was doing before */
353+
}
354+
return 0;
317355
}
318356

319-
void imap_clients_renew_idle(struct imap_session *imap)
357+
void imap_clients_renew_idle(struct imap_session *imap, struct imap_client *except)
320358
{
321359
struct stringlist deadclients;
322360
char *clientname;
@@ -330,7 +368,7 @@ void imap_clients_renew_idle(struct imap_session *imap)
330368
RWLIST_WRLOCK(&imap->clients);
331369
RWLIST_TRAVERSE_SAFE_BEGIN(&imap->clients, client, entry) {
332370
time_t maxage;
333-
if (!client->idling || client->dead) {
371+
if (!client->idling || client->dead || client == except) {
334372
continue;
335373
}
336374
/* This is when the connection may be terminated.
@@ -348,7 +386,10 @@ void imap_clients_renew_idle(struct imap_session *imap)
348386
/* The list is locked at the moment,
349387
* and creating a client entails acquiring that lock,
350388
* so for now, just keep track of what clients we removed,
351-
* and recreate them afterwards. */
389+
* and recreate them afterwards.
390+
*
391+
* The selected mailbox only matters for the foreground client,
392+
* so we don't need to care about it for these. */
352393
stringlist_push_tail(&deadclients, client->name);
353394
client_destroy(client);
354395
}
@@ -582,9 +623,8 @@ ssize_t __imap_client_send_log(struct imap_client *client, int log, const char *
582623
return -1;
583624
}
584625

585-
if (client->idling) {
626+
if (bbs_assertion_failed(!client->idling)) { /* Could stop idle now if this were to happen, but that would just mask a bug */
586627
bbs_warning("Client is currently idling while attempting to write '%.*s'", len, buf);
587-
bbs_soft_assert(0); /* Could stop idle now if this were to happen, but that would just mask a bug */
588628
}
589629

590630
if (log) {
@@ -600,12 +640,10 @@ int __imap_client_wait_response(struct imap_client *client, int fd, int ms, int
600640
int taglen;
601641
const char *tag = "tag";
602642

603-
if (!client->imap) {
643+
if (bbs_assertion_failed(client->imap != NULL)) {
604644
bbs_warning("No active IMAP client?\n"); /* Shouldn't happen... */
605-
bbs_soft_assert(0);
606-
} else if (strlen_zero(client->imap->tag)) {
645+
} else if (bbs_assertion_failed(!strlen_zero(client->imap->tag))) {
607646
bbs_warning("No active IMAP tag, using generic one\n");
608-
bbs_soft_assert(0);
609647
} else {
610648
tag = client->imap->tag;
611649
}
@@ -627,12 +665,10 @@ int __imap_client_send_wait_response(struct imap_client *client, int fd, int ms,
627665
va_list ap;
628666
const char *tag = "tag";
629667

630-
if (!client->imap) {
668+
if (bbs_assertion_failed(client->imap != NULL)) {
631669
bbs_warning("No active IMAP client?\n"); /* Shouldn't happen... */
632-
bbs_soft_assert(0);
633-
} else if (strlen_zero(client->imap->tag)) {
670+
} else if (bbs_assertion_failed(!strlen_zero(client->imap->tag))) {
634671
bbs_warning("No active IMAP tag, using generic one\n");
635-
bbs_soft_assert(0);
636672
} else {
637673
tag = client->imap->tag;
638674
}
@@ -652,17 +688,26 @@ int __imap_client_send_wait_response(struct imap_client *client, int fd, int ms,
652688

653689
#if 0
654690
/* Somewhat redundant since there's another debug right after */
655-
bbs_debug(6, "Passing through command %s (line %d) to remotely mapped '%s'\n", tag, lineno, client->virtprefix);
691+
bbs_debug(8, "Passing through command %s (line %d) to remotely mapped '%s'\n", tag, lineno, client->virtprefix);
656692
#else
657693
UNUSED(lineno);
658694
#endif
659695

660-
/* Only include this check if it's not the active client,
661-
* since a lot of pass through command code uses imap_client_send,
662-
* in which case this would be a false positive otherwise. */
663-
if (client != client->imap->client && client->idling) {
664-
bbs_warning("Client is currently idling while attempting to write '%s%s'", tagbuf, buf);
665-
bbs_soft_assert(0); /* Could stop idle now if this were to happen, but that would just mask a bug */
696+
if (bbs_assertion_failed(!client->idling)) {
697+
/* We shouldn't be idling. If we are, it means we're trying to send a command while IDLE,
698+
* which will fail since the only valid input to the server is "DONE".
699+
* For background clients, it's very clear it shouldn't, as it means we failed to call imap_client_idle_stop somewhere.
700+
* Even for foreground clients, it shouldn't happen. Although we do passthrough using imap_client_send to some extent,
701+
* imap_client_idle_start and imap_client_idle_stop are still called on paths where it's the foreground client,
702+
* instead of directly proxying it. The only time this should be true is after imap_poll returns. */
703+
bbs_warning("%s client %s was idling while attempting to write '%s%s'",
704+
client == client->imap->client ? "Foreground" : "Background", client->virtprefix, tagbuf, buf);
705+
/* Could simply stop idle now if this were to happen, but that would just mask a bug,
706+
* hence the assertion, but afterwards, try to rectify and fix so we can proceed. */
707+
if (imap_client_idle_stop(client)) {
708+
return -1;
709+
}
710+
/* Okay, now the client is no longer idling for sure */
666711
}
667712

668713
if (bbs_write(client->client.wfd, tagbuf, (unsigned int) taglen) < 0) {
@@ -924,7 +969,7 @@ struct imap_client *__imap_client_get_by_url(struct imap_session *imap, const ch
924969
* to keep these connections alive... */
925970
client->maxidlesec = 65; /* ~1 minute */
926971
} else {
927-
client->maxidlesec = 1800; /* 30 minutes */
972+
client->maxidlesec = MAX_IDLE_SECS; /* 30 minutes */
928973
}
929974

930975
client->lastactive = time(NULL); /* Mark as active since we just successfully did I/O with it */
@@ -1118,7 +1163,7 @@ int mailbox_remotely_mapped(struct imap_session *imap, const char *path)
11181163
return exists;
11191164
}
11201165

1121-
char *remote_mailbox_name(struct imap_client *client, char *restrict mailbox)
1166+
const char *remote_mailbox_name(struct imap_client *client, char *restrict mailbox)
11221167
{
11231168
char *tmp, *remotename = mailbox + client->virtprefixlen + 1;
11241169
/* This is some other server's problem to handle.

nets/net_imap/imap_client.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,12 @@ int imap_clients_next_idle_expiration(struct imap_session *imap);
6565
*/
6666
int imap_recreate_client(struct imap_session *imap, struct imap_client *client);
6767

68-
/*! \brief Renew IDLE on all idling clients that are close to expiring */
69-
void imap_clients_renew_idle(struct imap_session *imap);
68+
/*!
69+
* \brief Renew IDLE on all idling clients that are close to expiring
70+
* \param imap
71+
* \param except Renew IDLE on all clients except this one, if non-NULL
72+
*/
73+
void imap_clients_renew_idle(struct imap_session *imap, struct imap_client *except);
7074

7175
void imap_client_idle_notify(struct imap_client *client);
7276

@@ -148,4 +152,4 @@ struct imap_client *load_virtual_mailbox(struct imap_session *imap, const char *
148152
int mailbox_remotely_mapped(struct imap_session *imap, const char *path);
149153

150154
/*! \brief Convert a local mailbox name to its name on a remote server */
151-
char *remote_mailbox_name(struct imap_client *client, char *restrict mailbox);
155+
const char *remote_mailbox_name(struct imap_client *client, char *restrict mailbox);

nets/net_imap/imap_client_status.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ static int status_size_fetch_incremental(struct imap_client *client, const char
212212
return -1;
213213
}
214214
if (oldv != newv) {
215-
bbs_verb(4, "Remote UIDVALIDITY changed from %d to %d\n", oldv, newv);
215+
bbs_verb(4, "Remote UIDVALIDITY (%s on %s) changed from %d to %d\n", remotename, client->name, oldv, newv);
216216
return -1;
217217
}
218218
if (parse_status_item(old, "MESSAGES", &oldmessages) || parse_status_item(new, "MESSAGES", &newmessages)) {

0 commit comments

Comments
 (0)