From 85e120fa67a91b5156ecf386a880215993c892e0 Mon Sep 17 00:00:00 2001
From: Matthias Andree <matthias.andree@gmx.de>
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  <stdlib.h>
 #include  <limits.h>
 #include  <errno.h>
+#include  <stdbool.h>
 #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 */
 
