aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--NEWS10
-rw-r--r--fetchmail.h1
-rw-r--r--imap.c298
3 files changed, 170 insertions, 139 deletions
diff --git a/NEWS b/NEWS
index 1ff64180..d39ea062 100644
--- a/NEWS
+++ b/NEWS
@@ -49,6 +49,16 @@ removed from a 6.4.0 or newer release.)
fetchmail 6.3.14 (not yet released):
+# BUG FIXES
+* The IMAP client no longer skips messages from several IMAP servers including
+ Dovecot if fetchmail's "idle" is in use. Causes were that fetchmail (a)
+ ignored some untagged responses when it should not (b) relied on EXISTS
+ messages in response to EXPUNGE, which aren't mandated by RFC-3501 (the IMAP
+ standard) and aren't sent by Dovecot either.
+ Fix by Sunil Shetye (the fix also consolidates IMAP response handling,
+ improving overall robustness of the IMAP client), bug report and testing by
+ Matt Doran, with further hints from Timo Sirainen.
+
# TRANSLATION UPDATES
* [it] Italian, by Vincenzo Campanella
diff --git a/fetchmail.h b/fetchmail.h
index 702fcae5..c459bdca 100644
--- a/fetchmail.h
+++ b/fetchmail.h
@@ -145,6 +145,7 @@ char *strstr(const char *, const char *);
#define PS_RETAINED 26 /* message retained (internal use) */
#define PS_REPOLL 28 /* repoll immediately with changed parameters (internal use) */
#define PS_IDLETIMEOUT 29 /* timeout on imap IDLE (internal use) */
+#define PS_UNTAGGED 30 /* untagged response on imap command (internal use) */
/* output noise level */
#define O_SILENT 0 /* mute, max squelch, etc. */
diff --git a/imap.c b/imap.c
index 54309f7d..14566d43 100644
--- a/imap.c
+++ b/imap.c
@@ -49,7 +49,122 @@ static int actual_deletions = 0;
static int saved_timeout = 0, idle_timeout = 0;
static time_t idle_start_time = 0;
-static int imap_ok(int sock, char *argbuf)
+static int imap_untagged_response(int sock, const char *buf)
+/* interpret untagged status responses */
+{
+ /* For each individual check, use a BLANK before the word to avoid
+ * confusion with the \Recent flag or similar */
+ if (stage == STAGE_GETAUTH
+ && !strncmp(buf, "* CAPABILITY", 12))
+ {
+ strlcpy(capabilities, buf + 12, sizeof(capabilities));
+ }
+ else if (stage == STAGE_GETAUTH
+ && !strncmp(buf, "* PREAUTH", 9))
+ {
+ preauth = TRUE;
+ }
+ else if (stage != STAGE_LOGOUT
+ && !strncmp(buf, "* BYE", 5))
+ {
+ /* log the unexpected bye from server as we expect the
+ * connection to be cut-off after this */
+ if (outlevel > O_SILENT)
+ report(stderr, GT_("bye from imap server: %s"), buf + 5);
+ }
+ else if (strstr(buf, " EXISTS"))
+ {
+ count = atoi(buf+2);
+ /*
+ * Don't trust the message count passed by the server.
+ * Without this check, it might be possible to do a
+ * DNS-spoofing attack that would pass back a ridiculous
+ * count, and allocate a malloc area that would overlap
+ * a portion of the stack.
+ */
+ if ((unsigned)count > INT_MAX/sizeof(int))
+ {
+ report(stderr, GT_("bogus message count!"));
+ return(PS_PROTOCOL);
+ }
+ if ((recentcount = count - oldcount) < 0)
+ recentcount = 0;
+
+ /*
+ * Nasty kluge to handle RFC2177 IDLE. If we know we're idling
+ * we can't wait for the tag matching the IDLE; we have to tell the
+ * server the IDLE is finished by shipping back a DONE when we
+ * see an EXISTS. Only after that will a tagged response be
+ * shipped. The idling flag also gets cleared on a timeout.
+ */
+ if (stage == STAGE_IDLE)
+ {
+ /* If IDLE isn't supported, we were only sending NOOPs anyway. */
+ if (has_idle)
+ {
+ /* we do our own write and report here to disable tagging */
+ SockWrite(sock, "DONE\r\n", 6);
+ if (outlevel >= O_MONITOR)
+ report(stdout, "IMAP> DONE\n");
+ }
+
+ mytimeout = saved_timeout;
+ stage = STAGE_GETRANGE;
+ }
+ }
+ /* we now compute recentcount as a difference between
+ * new and old EXISTS, hence disable RECENT check */
+# if 0
+ else if (strstr(buf, " RECENT"))
+ {
+ recentcount = atoi(buf+2);
+ }
+# endif
+ else if (strstr(buf, " EXPUNGE"))
+ {
+ /* the response "* 10 EXPUNGE" means that the currently
+ * tenth (i.e. only one) message has been deleted */
+ if (atoi(buf+2) > 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 ((recentcount = count - oldcount) < 0)
+ recentcount = 0;
+ actual_deletions++;
+ }
+ }
+ /*
+ * The server may decide to make the mailbox read-only,
+ * which causes fetchmail to go into a endless loop
+ * fetching the same message over and over again.
+ *
+ * However, for check_only, we use EXAMINE which will
+ * mark the mailbox read-only as per the RFC.
+ *
+ * This checks for the condition and aborts if
+ * the mailbox is read-only.
+ *
+ * See RFC 2060 section 6.3.1 (SELECT).
+ * See RFC 2060 section 6.3.2 (EXAMINE).
+ */
+ else if (stage == STAGE_GETRANGE
+ && !check_only && strstr(buf, "[READ-ONLY]"))
+ {
+ return(PS_LOCKBUSY);
+ }
+ else
+ {
+ return(PS_UNTAGGED);
+ }
+ return(PS_SUCCESS);
+}
+
+static int imap_response(int sock, char *argbuf)
/* parse command response */
{
char buf[MSGBUFSIZE+1];
@@ -66,103 +181,23 @@ static int imap_ok(int sock, char *argbuf)
if (islower((unsigned char)*cp))
*cp = toupper((unsigned char)*cp);
- /* interpret untagged status responses
- * First check if we really have an untagged response, starting
- * with "*" SPACE. Then, for each individual check, use a BLANK
- * before the word to avoid confusion with the \Recent flag or
- * similar */
+ /* untagged responses start with "* " */
if (buf[0] == '*' && buf[1] == ' ') {
- if (strstr(buf, " CAPABILITY")) {
- strlcpy(capabilities, buf + 12, sizeof(capabilities));
- }
- else if (strstr(buf, " EXISTS"))
+ ok = imap_untagged_response(sock, buf);
+ if (ok == PS_UNTAGGED)
{
- count = atoi(buf+2);
- /*
- * Don't trust the message count passed by the server.
- * Without this check, it might be possible to do a
- * DNS-spoofing attack that would pass back a ridiculous
- * count, and allocate a malloc area that would overlap
- * a portion of the stack.
- */
- if ((unsigned)count > INT_MAX/sizeof(int))
+ if (argbuf && stage != STAGE_IDLE && tag[0] != '\0')
{
- report(stderr, GT_("bogus message count!"));
- return(PS_PROTOCOL);
- }
- if ((recentcount = count - oldcount) < 0)
- recentcount = 0;
-
- /*
- * Nasty kluge to handle RFC2177 IDLE. If we know we're idling
- * we can't wait for the tag matching the IDLE; we have to tell the
- * server the IDLE is finished by shipping back a DONE when we
- * see an EXISTS. Only after that will a tagged response be
- * shipped. The idling flag also gets cleared on a timeout.
- */
- if (stage == STAGE_IDLE)
- {
- /* If IDLE isn't supported, we were only sending NOOPs anyway. */
- if (has_idle)
- {
- /* we do our own write and report here to disable tagging */
- SockWrite(sock, "DONE\r\n", 6);
- if (outlevel >= O_MONITOR)
- report(stdout, "IMAP> DONE\n");
- }
-
- mytimeout = saved_timeout;
- stage = STAGE_FETCH;
- }
- }
- /* we now compute recentcount as a difference between
- * new and old EXISTS, hence disable RECENT check */
-# if 0
- else if (strstr(buf, " RECENT"))
- {
- recentcount = atoi(buf+2);
- }
-# endif
- else if (strstr(buf, " EXPUNGE"))
- {
- /* the response "* 10 EXPUNGE" means that the currently
- * tenth (i.e. only one) message has been deleted */
- if (atoi(buf+2) > 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 (recentcount > 0)
- recentcount--;
- actual_deletions++;
+ /* if there is an unmatched response, pass it back to
+ * the calling function for further analysis. The
+ * calling function should call imap_response() again
+ * to read the remaining response */
+ strcpy(argbuf, buf);
+ return(ok);
}
}
- else if (strstr(buf, " PREAUTH"))
- {
- preauth = TRUE;
- }
- /*
- * The server may decide to make the mailbox read-only,
- * which causes fetchmail to go into a endless loop
- * fetching the same message over and over again.
- *
- * However, for check_only, we use EXAMINE which will
- * mark the mailbox read-only as per the RFC.
- *
- * This checks for the condition and aborts if
- * the mailbox is read-only.
- *
- * See RFC 2060 section 6.3.1 (SELECT).
- * See RFC 2060 section 6.3.2 (EXAMINE).
- */
- else if (!check_only && strstr(buf, "[READ-ONLY]"))
- {
- return(PS_LOCKBUSY);
- }
+ else if (ok != PS_SUCCESS)
+ return(ok);
}
if (stage == STAGE_IDLE)
@@ -204,6 +239,8 @@ static int imap_ok(int sock, char *argbuf)
{
if (stage == STAGE_GETAUTH)
return(PS_AUTHFAIL); /* RFC2060, 6.2.2 */
+ else if (stage == STAGE_GETSIZES)
+ return(PS_SUCCESS); /* see comments in imap_getpartialsizes() */
else
return(PS_ERROR);
}
@@ -212,6 +249,16 @@ static int imap_ok(int sock, char *argbuf)
}
}
+static int imap_ok(int sock, char *argbuf)
+/* parse command response */
+{
+ int ok;
+
+ while ((ok = imap_response(sock, argbuf)) == PS_UNTAGGED)
+ ; /* wait for the tagged response */
+ return(ok);
+}
+
#ifdef NTLM_ENABLE
#include "ntlm.h"
@@ -713,7 +760,7 @@ static int imap_idle(int sock)
report(stdout, "IMAP> DONE\n");
/* reset stage and timeout here: we are not idling any more */
mytimeout = saved_timeout;
- stage = STAGE_FETCH;
+ stage = STAGE_GETRANGE;
/* get OK IDLE message */
ok = imap_ok(sock, NULL);
}
@@ -746,7 +793,7 @@ static int imap_idle(int sock)
/* restore normal timeout value */
set_timeout(0);
mytimeout = saved_timeout;
- stage = STAGE_FETCH;
+ stage = STAGE_GETRANGE;
return(ok);
}
@@ -864,14 +911,9 @@ static int imap_getrange(int sock,
/* don't count deleted messages, in case user enabled keep last time */
gen_send(sock, "SEARCH UNSEEN NOT DELETED");
- do {
- ok = gen_recv(sock, buf, sizeof(buf));
- if (ok != 0)
- {
- report(stderr, GT_("search for unseen messages failed\n"));
- return(PS_PROTOCOL);
- }
- else if ((cp = strstr(buf, "* SEARCH")))
+ while ((ok = imap_response(sock, buf)) == PS_UNTAGGED)
+ {
+ if ((cp = strstr(buf, "* SEARCH")))
{
char *ep;
@@ -904,8 +946,12 @@ static int imap_getrange(int sock,
}
}
}
- } while
- (tag[0] != '\0' && strncmp(buf, tag, strlen(tag)));
+ }
+ if (ok != 0)
+ {
+ report(stderr, GT_("search for unseen messages failed\n"));
+ return(ok);
+ }
if (outlevel >= O_DEBUG && unseen > 0)
report(stdout, GT_("%u is first unseen\n"), startcount);
@@ -923,6 +969,7 @@ static int imap_getpartialsizes(int sock, int first, int last, int *sizes)
/* capture the sizes of messages #first-#last */
{
char buf [MSGBUFSIZE+1];
+ int ok;
/*
* Some servers (as in, PMDF5.1-9.1 under OpenVMS 6.1)
@@ -954,7 +1001,7 @@ static int imap_getpartialsizes(int sock, int first, int last, int *sizes)
* * 1 FETCH (BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 35 3))
* A006 OK FETCH completed.
*
- * To get around this, we terminate the read loop on a NO and count
+ * To get around this, we treat the final NO as success and count
* on the fact that the sizes array has been preinitialized with a
* known-bad size value.
*/
@@ -969,23 +1016,12 @@ static int imap_getpartialsizes(int sock, int first, int last, int *sizes)
gen_send(sock, "FETCH %d:%d RFC822.SIZE", first, last);
else /* no unseen messages! */
return(PS_SUCCESS);
- for (;;)
+ while ((ok = imap_response(sock, buf)) == PS_UNTAGGED)
{
unsigned int size;
- int ok, num;
- char *cp;
+ int num;
- if ((ok = gen_recv(sock, buf, sizeof(buf))))
- return(ok);
- /* we want response matching to be case-insensitive */
- for (cp = buf; *cp; cp++)
- *cp = toupper((unsigned char)*cp);
- /* an untagged NO means that a message was not readable */
- if (strstr(buf, "* NO"))
- ;
- else if (strstr(buf, "OK") || strstr(buf, "NO"))
- break;
- else if (sscanf(buf, "* %d FETCH (RFC822.SIZE %u)", &num, &size) == 2
+ if (sscanf(buf, "* %d FETCH (RFC822.SIZE %u)", &num, &size) == 2
/* some servers (like mail.internode.on.net bld-mail04) return UID information here
*
* IMAP> A0005 FETCH 1 RFC822.SIZE
@@ -1002,8 +1038,7 @@ static int imap_getpartialsizes(int sock, int first, int last, int *sizes)
GT_("Warning: ignoring bogus data for message sizes returned by the server.\n"));
}
}
-
- return(PS_SUCCESS);
+ return(ok);
}
static int imap_getsizes(int sock, int count, int *sizes)
@@ -1194,24 +1229,9 @@ static int imap_trail(int sock, struct query *ctl, const char *tag)
/* number -= expunged; */
(void)ctl;
- for (;;)
- {
- char buf[MSGBUFSIZE+1], *t;
- int ok;
-
- if ((ok = gen_recv(sock, buf, sizeof(buf))))
- return(ok);
-
- /* UW IMAP returns "OK FETCH", Cyrus returns "OK Completed" */
- if (strncmp(buf, tag, strlen(tag)) == 0) {
- t = buf + strlen(tag);
- t += strspn(t, " \t");
- if (strncmp(t, "OK", 2) == 0)
- break;
- }
- }
+ (void)tag;
- return(PS_SUCCESS);
+ return imap_ok(sock, NULL);
}
static int imap_delete(int sock, struct query *ctl, int number)