]> Pileus Git - ~andy/fetchmail/blobdiff - imap.c
Warn about Novonyx.
[~andy/fetchmail] / imap.c
diff --git a/imap.c b/imap.c
index df787da6208f7c10dbf362c47961cfc544539864..0654bdeb5c6b8121a2799ca37af6ebcc733cc30a 100644 (file)
--- a/imap.c
+++ b/imap.c
@@ -62,23 +62,17 @@ extern char *strstr();      /* needed on sysV68 R3V7.1. */
 #define IMAP4          0       /* IMAP4 rev 0, RFC1730 */
 #define IMAP4rev1      1       /* IMAP4 rev 1, RFC2060 */
 
-static int imap_phase;
-#define PHASE_GETAUTH  0
-#define PHASE_GETRANGE 1
-#define PHASE_GETSIZES 2
-#define PHASE_FETCH    3
-#define PHASE_LOGOUT   4
-
-static int count, seen, recent, unseen, deletions, imap_version, preauth; 
-static int expunged, expunge_period;
+static int count, unseen, deletions, imap_version, preauth; 
+static int expunged, expunge_period, saved_timeout;
+static flag do_idle;
 static char capabilities[MSGBUFSIZE+1];
+static unsigned int *unseen_messages;
 
 int imap_ok(int sock, char *argbuf)
 /* parse command response */
 {
-    char buf [MSGBUFSIZE+1];
+    char buf[MSGBUFSIZE+1];
 
-    seen = 0;
     do {
        int     ok;
        char    *cp;
@@ -94,27 +88,28 @@ int imap_ok(int sock, char *argbuf)
        /* interpret untagged status responses */
        if (strstr(buf, "* CAPABILITY"))
            strncpy(capabilities, buf + 12, sizeof(capabilities));
-       if (strstr(buf, "EXISTS"))
-           count = atoi(buf+2);
-       if (strstr(buf, "RECENT"))
-           recent = atoi(buf+2);
-       if (strstr(buf, "UNSEEN"))
+       else if (strstr(buf, "EXISTS"))
        {
-           char        *cp;
-
+           count = atoi(buf+2);
            /*
-            * Handle both "* 42 UNSEEN" (if tha ever happens) and 
-            * "* OK [UNSEEN 42] 42". Note that what this gets us is
-            * a minimum index, not a count.
+            * 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.
             */
-           unseen = 0;
-           for (cp = buf; *cp && !isdigit(*cp); cp++)
-               continue;
-           unseen = atoi(cp);
+           if (stage == STAGE_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;
+           }
        }
-       if (strstr(buf, "FLAGS"))
-           seen = (strstr(buf, "SEEN") != (char *)NULL);
-       if (strstr(buf, "PREAUTH"))
+       else if (strstr(buf, "PREAUTH"))
            preauth = TRUE;
     } while
        (tag[0] != '\0' && strncmp(buf, tag, strlen(tag)));
@@ -135,14 +130,7 @@ int imap_ok(int sock, char *argbuf)
        while (isspace(*cp))
            cp++;
 
-       if (strncmp(cp, "PREAUTH", 2) == 0)
-       {
-           if (argbuf)
-               strcpy(argbuf, cp);
-           preauth = TRUE;
-           return(PS_SUCCESS);
-       }
-       else if (strncmp(cp, "OK", 2) == 0)
+        if (strncmp(cp, "OK", 2) == 0)
        {
            if (argbuf)
                strcpy(argbuf, cp);
@@ -152,7 +140,7 @@ int imap_ok(int sock, char *argbuf)
            return(PS_ERROR);
        else if (strncmp(cp, "NO", 2) == 0)
        {
-           if (imap_phase == PHASE_GETAUTH) 
+           if (stage == STAGE_GETAUTH) 
                return(PS_AUTHFAIL);    /* RFC2060, 6.2.2 */
            else
                return(PS_ERROR);
@@ -327,7 +315,7 @@ static int do_rfc1731(int sock, char *truename)
 
     memcpy(session, credentials.session, sizeof session);
     memset(&credentials, 0, sizeof credentials);
-    des_key_sched(&session, schedule);
+    des_key_sched(session, schedule);
 
     result = krb_get_tf_fullname(TKT_FILE, tktuser, tktinst, tktrealm);
     if (result) {
@@ -335,12 +323,18 @@ static int do_rfc1731(int sock, char *truename)
        return PS_AUTHFAIL;
     }
 
+#ifdef __UNUSED__
+    /*
+     * Andrew H. Chatham <andrew.chatham@duke.edu> alleges that this check
+     * is not necessary and has consistently been messing him up.
+     */
     if (strcmp(tktuser, user) != 0) {
        report(stderr, 
               _("principal %s in ticket does not match -u %s\n"), tktuser,
                user);
        return PS_AUTHFAIL;
     }
+#endif /* __UNUSED__ */
 
     if (tktinst[0]) {
        report(stderr, 
@@ -863,8 +857,6 @@ int imap_getauth(int sock, struct query *ctl, char *greeting)
 {
     int ok = 0;
 
-    imap_phase = PHASE_GETAUTH;
-
     /* probe to see if we're running IMAP4 and can use RFC822.PEEK */
     capabilities[0] = '\0';
     if ((ok = gen_transact(sock, "CAPABILITY")) == PS_SUCCESS)
@@ -903,8 +895,26 @@ int imap_getauth(int sock, struct query *ctl, char *greeting)
     else
        expunge_period = 1;
 
-    if (preauth)
-       return(PS_SUCCESS);
+    /* 
+     * If either (a) we saw a PREAUTH token in the greeting, or
+     * (b) the user specified ssh preauthentication, then we're done.
+     */
+    if (preauth || ctl->server.preauthenticate == A_SSH)
+    {
+        preauth = FALSE;  /* reset for the next session */
+        return(PS_SUCCESS);
+    }
+
+    /* 
+     * Handle idling.  We depend on coming through here on startup
+     * and after each timeout (including timeouts during idles).
+     */
+    if (strstr(capabilities, "IDLE") && ctl->idle)
+    {
+       do_idle = TRUE;
+       if (outlevel >= O_VERBOSE)
+           report(stdout, "will idle after poll\n");
+    }
 
 #if OPIE_ENABLE
     if ((ctl->server.protocol == P_IMAP) && strstr(capabilities, "AUTH=X-OTP"))
@@ -1034,6 +1044,16 @@ static int internal_expunge(int sock)
     return(PS_SUCCESS);
 }
 
+static int imap_idle(int sock)
+/* start an RFC2177 IDLE */
+{
+    stage = STAGE_IDLE;
+    saved_timeout = mytimeout;
+    mytimeout = 0;
+
+    return (gen_transact(sock, "IDLE"));
+}
+
 static int imap_getrange(int sock, 
                         struct query *ctl, 
                         const char *folder, 
@@ -1041,11 +1061,10 @@ static int imap_getrange(int sock,
 /* get range of messages to be fetched */
 {
     int ok;
-
-    imap_phase = PHASE_GETRANGE;
+    char buf[MSGBUFSIZE+1], *cp;
 
     /* find out how many messages are waiting */
-    *bytes = recent = unseen = -1;
+    *bytes = -1;
 
     if (pass > 1)
     {
@@ -1057,48 +1076,93 @@ static int imap_getrange(int sock,
         */
        ok = 0;
        if (deletions && expunge_period != 1)
-           internal_expunge(sock);
+           ok = internal_expunge(sock);
        count = -1;
+       if (do_idle)
+           ok = imap_idle(sock);
        if (ok || gen_transact(sock, "NOOP"))
        {
            report(stderr, _("re-poll failed\n"));
            return(ok);
        }
-       else if (count == -1)   /* no EXISTS response to NOOP */
+       else if (count == -1)   /* no EXISTS response to NOOP/IDLE */
        {
-           count = recent = 0;
-           unseen = -1;
+           count = 0;
        }
+       if (outlevel >= O_DEBUG)
+           report(stdout, "%d messages waiting after re-poll\n", count);
     }
     else
     {
-       if (!check_only)
-           ok = gen_transact(sock, "SELECT %s", folder ? folder : "INBOX");
-       else
-           ok = gen_transact(sock, "EXAMINE %s", folder ? folder : "INBOX");
+       ok = gen_transact(sock, 
+                         check_only ? "EXAMINE \"%s\"" : "SELECT \"%s\"",
+                         folder ? folder : "INBOX");
        if (ok != 0)
        {
            report(stderr, _("mailbox selection failed\n"));
            return(ok);
        }
+       else if (outlevel >= O_DEBUG)
+           report(stdout, "%d messages waiting after first poll\n", count);
+
+       /* no messages?  then we may need to idle until we get some */
+       if (count == 0 && do_idle)
+           imap_idle(sock);
     }
 
     *countp = count;
 
-    /*
-     * Note: because IMAP has an is_old method, this number is used
-     * only for the "X messages (Y unseen)" notification.  Accordingly
-     * it doesn't matter much that it can be wrong (e.g. if we see an
-     * UNSEEN response but not all messages above the first UNSEEN one
-     * are likewise).
-     */
-    if (unseen >= 0)           /* optional, but better if we see it */
-       *newp = count - unseen + 1;
-    else if (recent >= 0)      /* mandatory */
-       *newp = recent;
-    else
-       *newp = -1;             /* should never happen, RECENT is mandatory */ 
+    /* OK, now get a count of unseen messages and their indices */
+    if (!ctl->fetchall && count > 0)
+    {
+       if (unseen_messages)
+           free(unseen_messages);
+       unseen_messages = xmalloc(count * sizeof(unsigned int));
+       memset(unseen_messages, 0, count * sizeof(unsigned int));
+       unseen = 0;
+
+       gen_send(sock, "SEARCH UNSEEN");
+       do {
+           ok = gen_recv(sock, buf, sizeof(buf));
+           if (ok != 0)
+           {
+               report(stderr, _("search for unseen messages failed\n"));
+               return(PS_PROTOCOL);
+           }
+           else if ((cp = strstr(buf, "* SEARCH")))
+           {
+               char    *ep;
+
+               cp += 8;        /* skip "* SEARCH" */
+
+               while (*cp && unseen < count)
+               {
+                   /* skip whitespace */
+                   while (*cp && isspace(*cp))
+                       cp++;
+                   if (*cp) 
+                   {
+                       /*
+                        * Message numbers are between 1 and 2^32 inclusive,
+                        * so unsigned int is large enough.
+                        */
+                       unseen_messages[unseen]=(unsigned int)strtol(cp,&ep,10);
+
+                       if (outlevel >= O_DEBUG)
+                           report(stdout, 
+                                  _("%u is unseen\n"), 
+                                  unseen_messages[unseen]);
+               
+                       unseen++;
+                       cp = ep;
+                   }
+               }
+           }
+       } while
+           (tag[0] != '\0' && strncmp(buf, tag, strlen(tag)));
+    }
 
+    *newp = unseen;
     expunged = 0;
 
     return(PS_SUCCESS);
@@ -1109,12 +1173,39 @@ static int imap_getsizes(int sock, int count, int *sizes)
 {
     char buf [MSGBUFSIZE+1];
 
-    imap_phase = PHASE_GETSIZES;
-
     /*
      * Some servers (as in, PMDF5.1-9.1 under OpenVMS 6.1)
      * won't accept 1:1 as valid set syntax.  Some implementors
      * should be taken out and shot for excessive anality.
+     *
+     * Microsoft Exchange (brain-dead piece of crap that it is) 
+     * sometimes gets its knickers in a knot about bodiless messages.
+     * You may see responses like this:
+     *
+     * fetchmail: IMAP> A0004 FETCH 1:9 RFC822.SIZE
+     * fetchmail: IMAP< * 2 FETCH (RFC822.SIZE 1187)
+     * fetchmail: IMAP< * 3 FETCH (RFC822.SIZE 3954)
+     * fetchmail: IMAP< * 4 FETCH (RFC822.SIZE 1944)
+     * fetchmail: IMAP< * 5 FETCH (RFC822.SIZE 2933)
+     * fetchmail: IMAP< * 6 FETCH (RFC822.SIZE 1854)
+     * fetchmail: IMAP< * 7 FETCH (RFC822.SIZE 34054)
+     * fetchmail: IMAP< * 8 FETCH (RFC822.SIZE 5561)
+     * fetchmail: IMAP< * 9 FETCH (RFC822.SIZE 1101)
+     * fetchmail: IMAP< A0004 NO The requested item could not be found.
+     *
+     * This means message 1 has only headers.  For kicks and grins
+     * you can telnet in and look:
+     * A003 FETCH 1 FULL
+     * A003 NO The requested item could not be found.
+     * A004 fetch 1 rfc822.header
+     * A004 NO The requested item could not be found.
+     * A006 FETCH 1 BODY
+     * * 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
+     * on the fact that the sizes array has been preinitialized with a
+     * known-bad size value.
      */
     if (count == 1)
        gen_send(sock, "FETCH 1 RFC822.SIZE", count);
@@ -1126,27 +1217,34 @@ static int imap_getsizes(int sock, int count, int *sizes)
 
        if ((ok = gen_recv(sock, buf, sizeof(buf))))
            return(ok);
-       if (strstr(buf, "OK"))
+       else if (strstr(buf, "OK") || strstr(buf, "NO"))
            break;
        else if (sscanf(buf, "* %d FETCH (RFC822.SIZE %d)", &num, &size) == 2)
            sizes[num - 1] = size;
     }
 
-    imap_phase = PHASE_FETCH;
-
     return(PS_SUCCESS);
 }
 
 static int imap_is_old(int sock, struct query *ctl, int number)
 /* is the given message old? */
 {
-    int ok;
+    flag seen = TRUE;
+    int i;
 
-    /* expunges change the fetch numbers */
-    number -= expunged;
+    /* 
+     * Expunges change the fetch numbers, but unseen_messages contains
+     * indices from before any expungees were done.  So neither the
+     * argument nor the values in message_sequence need to be decremented.
+     */
 
-    if ((ok = gen_transact(sock, "FETCH %d FLAGS", number)) != 0)
-       return(PS_ERROR);
+    seen = TRUE;
+    for (i = 0; i < unseen; i++)
+       if (unseen_messages[i] == number)
+       {
+           seen = FALSE;
+           break;
+       }
 
     return(seen);
 }
@@ -1200,6 +1298,20 @@ static int imap_fetch_body(int sock, struct query *ctl, int number, int *lenp)
      * In that case, marking the seen flag is the only way to prevent the
      * message from being re-fetched on subsequent runs (and according
      * to RFC2060 p.43 this fetch should set Seen as a side effect).
+     *
+     * According to RFC2060, and Mark Crispin the IMAP maintainer,
+     * FETCH %d BODY[TEXT] and RFC822.TEXT are "functionally 
+     * equivalent".  However, we know of at least one server that
+     * treats them differently in the presence of MIME attachments;
+     * the latter form downloads the attachment, the former does not.
+     * The server is InterChange, and the fool who implemented this
+     * misfeature ought to be strung up by his thumbs.  
+     *
+     * When I tried working around this by disabling use of the 4rev1 form,
+     * I found that doing this breaks operation with M$ Exchange.
+     * Annoyingly enough, Exchange's refusal to cope is technically legal
+     * under RFC2062.  Trust Microsoft, the Great Enemy of interoperability
+     * standards, to find a way to make standards compliance irritating....
      */
     switch (imap_version)
     {
@@ -1331,12 +1443,16 @@ static int imap_delete(int sock, struct query *ctl, int number)
 static int imap_logout(int sock, struct query *ctl)
 /* send logout command */
 {
-    imap_phase = PHASE_LOGOUT;
-
     /* if any un-expunged deletions remain, ship an expunge now */
     if (deletions)
        internal_expunge(sock);
 
+#ifdef USE_SEARCH
+    /* Memory clean-up */
+    if (unseen_messages)
+       free(unseen_messages);
+#endif /* USE_SEARCH */
+
     return(gen_transact(sock, "LOGOUT"));
 }