From 85e120fa67a91b5156ecf386a880215993c892e0 Mon Sep 17 00:00:00 2001 From: Matthias Andree Date: Wed, 24 Jun 2026 22:26:40 +0200 Subject: [PATCH] IMAP: abort session on unexpected EXPUNGE responses. This is required because an EXPUNGE changes all message numbers, so we must be sure not to mark a wrong message as seen or deleted and possibly lose it on the next EXPUNGE. Earl Chew reported via #91 that fetchmail complains about an unexpected count of EXPUNGE responses already in response to a STORE for the \Deleted flag, and turns out this is not even sufficient. --- imap.c | 80 +++++++++++++++++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 35 deletions(-) --- ./imap.c.orig 2021-10-31 06:54:24.000000000 -0500 +++ ./imap.c 2026-06-25 15:32:42.476294241 -0500 @@ -16,6 +16,7 @@ #include #include #include +#include #endif #include "socket.h" @@ -37,6 +38,7 @@ static int imap_version = IMAP4; static flag has_idle = FALSE; static int expunge_period = 1; +static bool in_expunge = false; static void clear_sessiondata(void) { /* must match defaults above */ @@ -45,6 +47,7 @@ imap_version = IMAP4; has_idle = FALSE; expunge_period = 1; + in_expunge = false; } /* the next ones need to be kept in synch - C89 does not consider strlen() @@ -198,30 +201,34 @@ # endif else if (strstr(buf, " EXPUNGE")) { - unsigned long u; char *t; - /* the response "* 10 EXPUNGE" means that the currently - * tenth (i.e. only one) message has been deleted */ - errno = 0; - u = strtoul(buf+2, &t, 10); - if (errno /* conversion error */ || t == buf+2 /* no number found */) { - report(stderr, GT_("bogus EXPUNGE count in \"%s\"!"), buf); - return PS_PROTOCOL; - } - if (u > 0) - { - if (count > 0) - count--; - if (oldcount > 0) - oldcount--; - /* We do expect an EXISTS response immediately - * after this, so this updation of recentcount is - * just a precaution! + if (!in_expunge) { + report(stderr, GT_("Unexpected EXPUNGE response from IMAP server: \"%s\". fetchmail must abort the session because message numbers are desynchronized now.\n"), buf); + return PS_PROTOCOL; + } + unsigned long u; char *t; + /* the response "* 10 EXPUNGE" means that the currently + * tenth (i.e. only one) message has been deleted */ + errno = 0; + u = strtoul(buf+2, &t, 10); + if (errno /* conversion error */ || t == buf+2 /* no number found */) { + report(stderr, GT_("bogus EXPUNGE message number in untagged response \"%s\"!"), buf); + return PS_PROTOCOL; + } + if (u > 0) + { + if (count > 0) + count--; + if (oldcount > 0) + oldcount--; + /* We do expect an EXISTS response immediately + * after this, so this updation of recentcount is + * just a precaution! * XXX FIXME: per RFC 3501, 7.4.1. EXPUNGE Reponse * on Page 73, an EXISTS response is not required */ - if ((recentcount = count - oldcount) < 0) - recentcount = 0; - actual_deletions++; - } + if ((recentcount = count - oldcount) < 0) + recentcount = 0; + actual_deletions++; + } } /* * The server may decide to make the mailbox read-only, @@ -333,7 +340,7 @@ while (isspace((unsigned char)*cp)) cp++; - if (strncasecmp(cp, "OK", 2) == 0) + if (strncasecmp(cp, "OK", 2) == 0) { if (argbuf) strcpy(argbuf, cp); @@ -468,9 +475,10 @@ * after every message unless user said otherwise. */ if (NUM_SPECIFIED(ctl->expunge)) - expunge_period = NUM_VALUE_OUT(ctl->expunge); + expunge_period = NUM_VALUE_OUT(ctl->expunge); else - expunge_period = 1; + expunge_period = 1; + in_expunge = false; /* check if imap_ok() has already parsed CAPABILITY from the greeting when * driver.c ran it on the server's greeting message - note this must match @@ -749,12 +757,15 @@ static int internal_expunge(int sock) /* ship an expunge, resetting associated counters */ { - int ok; + int ok; actual_deletions = 0; - if ((ok = gen_transact(sock, "EXPUNGE"))) - return(ok); + in_expunge = true; + ok = gen_transact(sock, "EXPUNGE"); + in_expunge = false; + if (ok != PS_SUCCESS) + return ok; /* if there is a mismatch between the number of mails which should * have been expunged and the number of mails actually expunged, @@ -765,17 +776,17 @@ * every subsequent mail */ if (deletions > 0 && deletions != actual_deletions) { - report(stderr, - GT_("mail expunge mismatch (%d actual != %d expected)\n"), - actual_deletions, deletions); - deletions = 0; - return(PS_ERROR); + report(stderr, + GT_("mail expunge mismatch (%d actual != %d expected)\n"), + actual_deletions, deletions); + deletions = 0; + return(PS_ERROR); } expunged += deletions; deletions = 0; -#ifdef IMAP_UID /* not used */ +#ifdef IMAP_UID /* not used */ expunge_uids(ctl); #endif /* IMAP_UID */