]> granicus.if.org Git - mutt/commitdiff
Add $imap_fetch_chunk_size to allow FETCHing new headers in chunks.
authorKevin McCarthy <kevin@8t8.us>
Sun, 19 Aug 2018 16:25:53 +0000 (09:25 -0700)
committerKevin McCarthy <kevin@8t8.us>
Mon, 22 Apr 2019 22:27:34 +0000 (15:27 -0700)
For extremely large mailboxes, some implementations will time out just
while fetching the new headers, because the client doesn't send any
commands for 30 minutes while downloading the large number of headers.

Rewrite imap_fetch_msn_seqset() to return chunks of size
$imap_fetch_chunk_size.

The change requires trusting the server will follow the RFC and not
send an EXPUNGE during or between the FETCH chunks; otherwise we'll
miss MSNs between the chunks because the shift.

We could in theory continue to set "msn_begin = idata->max_msn + 1",
but that makes the assumption there are no holes in the header cache
that we are filling in during a chunk.  Personally I am dubious about
"header cache holes", but the IMAP code has explicitly mentioned and
handled them since prior to my involvement.

Since the RFC forbids the interleaving EXPUNGE I believe it's safe
enough to set "msn_begin = fetch_msn_end + 1" until proven otherwise.

globals.h
imap/message.c
init.h

index facb2ea8e947e9ab4604ea740cc3b787b3f4f6f5..e4eb026869513d31610ac34894d1f2e59482a682 100644 (file)
--- a/globals.h
+++ b/globals.h
@@ -240,6 +240,7 @@ WHERE LIST *SidebarWhitelist INITVAL(0);
 #endif
 
 #ifdef USE_IMAP
+WHERE long  ImapFetchChunkSize;
 WHERE short ImapKeepalive;
 WHERE short ImapPipelineDepth;
 WHERE short ImapPollTimeout;
index 662bf81a496e65e9fbe562b808497787e8001075..5c088cfc361ab6a42bf4061ea72cd6f060852503 100644 (file)
@@ -27,6 +27,7 @@
 #include <errno.h>
 #include <stdlib.h>
 #include <ctype.h>
+#include <limits.h>
 
 #include "mutt.h"
 #include "imap_private.h"
@@ -137,22 +138,41 @@ static void imap_alloc_uid_hash (IMAP_DATA *idata, unsigned int msn_count)
 
 /* Generates a more complicated sequence set after using the header cache,
  * in case there are missing MSNs in the middle.
- *
- * There is a suggested limit of 1000 bytes for an IMAP client request.
- * Ideally, we would generate multiple requests if the number of ranges
- * is too big, but for now just abort to using the whole range.
  */
-static void imap_fetch_msn_seqset (BUFFER *b, IMAP_DATA *idata, unsigned int msn_begin,
-                                   unsigned int msn_end)
+static unsigned int imap_fetch_msn_seqset (BUFFER *b, IMAP_DATA *idata, int evalhc,
+                                           unsigned int msn_begin, unsigned int msn_end,
+                                           unsigned int *fetch_msn_end)
 {
-  int chunks = 0;
+  unsigned int max_headers_per_fetch = UINT_MAX;
+  int first_chunk = 1;
   int state = 0;  /* 1: single msn, 2: range of msn */
-  unsigned int msn, range_begin, range_end;
+  unsigned int msn, range_begin, range_end, msn_count = 0;
+
+  mutt_buffer_clear (b);
+  if (msn_end < msn_begin)
+    return 0;
+
+  if (ImapFetchChunkSize > 0)
+    max_headers_per_fetch = ImapFetchChunkSize;
+
+  if (!evalhc)
+  {
+    if (msn_end - msn_begin + 1 <= max_headers_per_fetch)
+      *fetch_msn_end = msn_end;
+    else
+      *fetch_msn_end = msn_begin + max_headers_per_fetch - 1;
+    mutt_buffer_printf (b, "%u:%u", msn_begin, *fetch_msn_end);
+    return (*fetch_msn_end - msn_begin + 1);
+  }
 
   for (msn = msn_begin; msn <= msn_end + 1; msn++)
   {
-    if (msn <= msn_end && !idata->msn_index[msn-1])
+    if (msn_count < max_headers_per_fetch &&
+        msn <= msn_end &&
+        !idata->msn_index[msn-1])
     {
+      msn_count++;
+
       switch (state)
       {
         case 1:            /* single: convert to a range */
@@ -169,25 +189,27 @@ static void imap_fetch_msn_seqset (BUFFER *b, IMAP_DATA *idata, unsigned int msn
     }
     else if (state)
     {
-      if (chunks++)
+      if (first_chunk)
+        first_chunk = 0;
+      else
         mutt_buffer_addch (b, ',');
-      if (chunks == 150)
-        break;
 
       if (state == 1)
         mutt_buffer_add_printf (b, "%u", range_begin);
       else if (state == 2)
         mutt_buffer_add_printf (b, "%u:%u", range_begin, range_end);
       state = 0;
+
+      if ((mutt_buffer_len (b) > 500) ||
+          msn_count >= max_headers_per_fetch)
+        break;
     }
   }
 
-  /* Too big.  Just query the whole range then. */
-  if (chunks == 150 || mutt_strlen (b->data) > 500)
-  {
-    mutt_buffer_clear (b);
-    mutt_buffer_add_printf (b, "%u:%u", msn_begin, msn_end);
-  }
+  /* The loop index goes one past to terminate the range if needed. */
+  *fetch_msn_end = msn - 1;
+
+  return msn_count;
 }
 
 /* imap_read_headers:
@@ -686,7 +708,7 @@ static int read_headers_fetch_new (IMAP_DATA *idata, unsigned int msn_begin,
   char tempfile[_POSIX_PATH_MAX];
   FILE *fp = NULL;
   IMAP_HEADER h;
-  BUFFER *b;
+  BUFFER *b = NULL;
   static const char * const want_headers = "DATE FROM SENDER SUBJECT TO CC MESSAGE-ID REFERENCES CONTENT-TYPE CONTENT-DESCRIPTION IN-REPLY-TO REPLY-TO LINES LIST-POST X-LABEL";
 
   ctx = idata->ctx;
@@ -723,24 +745,26 @@ static int read_headers_fetch_new (IMAP_DATA *idata, unsigned int msn_begin,
   mutt_progress_init (&progress, _("Fetching message headers..."),
                      MUTT_PROGRESS_MSG, ReadInc, msn_end);
 
-  while (msn_begin <= msn_end && fetch_msn_end < msn_end)
+  b = mutt_buffer_pool_get ();
+
+  /* NOTE:
+   *   The (fetch_msn_end < msn_end) used to be important to prevent
+   *   an infinite loop, in the event the server did not return all
+   *   the headers (due to a pending expunge, for example).
+   *
+   *   I believe the new chunking imap_fetch_msn_seqset()
+   *   implementation and "msn_begin = fetch_msn_end + 1" assignment
+   *   at the end of the loop makes the comparison unneeded, but to be
+   *   cautious I'm keeping it.
+   */
+  while ((fetch_msn_end < msn_end) &&
+         imap_fetch_msn_seqset (b, idata, evalhc, msn_begin, msn_end,
+                                &fetch_msn_end))
   {
-    b = mutt_buffer_new ();
-    if (evalhc)
-    {
-      /* In case there are holes in the header cache. */
-      evalhc = 0;
-      imap_fetch_msn_seqset (b, idata, msn_begin, msn_end);
-    }
-    else
-      mutt_buffer_add_printf (b, "%u:%u", msn_begin, msn_end);
-
-    fetch_msn_end = msn_end;
     safe_asprintf (&cmd, "FETCH %s (UID FLAGS INTERNALDATE RFC822.SIZE %s)",
-                   b->data, hdrreq);
+                   mutt_b2s (b), hdrreq);
     imap_cmd_start (idata, cmd);
     FREE (&cmd);
-    mutt_buffer_free (&b);
 
     rc = IMAP_CMD_CONTINUE;
     for (msgno = msn_begin; rc == IMAP_CMD_CONTINUE; msgno++)
@@ -840,17 +864,9 @@ static int read_headers_fetch_new (IMAP_DATA *idata, unsigned int msn_begin,
         goto bail;
     }
 
-    /* In case we get new mail while fetching the headers.
-     *
-     * Note: The RFC says we shouldn't get any EXPUNGE responses in the
-     * middle of a FETCH.  But just to be cautious, use the current state
-     * of max_msn, not fetch_msn_end to set the next start range.
-     */
+    /* In case we get new mail while fetching the headers. */
     if (idata->reopen & IMAP_NEWMAIL_PENDING)
     {
-      /* update to the last value we actually pulled down */
-      fetch_msn_end = idata->max_msn;
-      msn_begin = idata->max_msn + 1;
       msn_end = idata->newMailCount;
       while (msn_end > ctx->hdrmax)
         mx_alloc_memory (ctx);
@@ -858,11 +874,24 @@ static int read_headers_fetch_new (IMAP_DATA *idata, unsigned int msn_begin,
       idata->reopen &= ~IMAP_NEWMAIL_PENDING;
       idata->newMailCount = 0;
     }
+
+    /* Note: RFC3501 section 7.4.1 and RFC7162 section 3.2.10.2 say we
+     * must not get any EXPUNGE/VANISHED responses in the middle of a
+     * FETCH, nor when no command is in progress (e.g. between the
+     * chunked FETCH commands).  We previously tried to be robust by
+     * setting:
+     *   msn_begin = idata->max_msn + 1;
+     * but with chunking (and the mythical header cache holes) this
+     * may not be correct.  So here we must assume the msn values have
+     * not been altered during or after the fetch.
+     */
+    msn_begin = fetch_msn_end + 1;
   }
 
   retval = 0;
 
 bail:
+  mutt_buffer_pool_release (&b);
   safe_fclose (&fp);
   FREE (&hdrreq);
 
diff --git a/init.h b/init.h
index a6b417c1fb9ca32641c2b76d3e22437b5d6983cc..8bfe2684090ed4b47de49fda8ebb648b56012960 100644 (file)
--- a/init.h
+++ b/init.h
@@ -1360,6 +1360,15 @@ struct option_t MuttVars[] = {
   ** as folder separators for displaying IMAP paths. In particular it
   ** helps in using the ``='' shortcut for your \fIfolder\fP variable.
   */
+  { "imap_fetch_chunk_size",   DT_LNUM, R_NONE, UL &ImapFetchChunkSize, 0 },
+  /*
+  ** .pp
+  ** When set to a value greater than 0, new headers will be downloaded
+  ** in sets of this size.  If you have a very large mailbox, this might
+  ** prevent a timeout and disconnect when opening the mailbox, by sending
+  ** a FETCH per set of this size instead of a single FETCH for all new
+  ** headers.
+  */
   { "imap_headers",    DT_STR, R_INDEX, UL &ImapHeaders, UL 0},
   /*
   ** .pp