]> diplodocus.org Git - nmh/blobdiff - uip/imaptest.c
Move the opening brace of a C function to its own line.
[nmh] / uip / imaptest.c
index 0ae9c2db7607658cbc60fb717bab9a5685194535..01d3ee52459371ece81e22929dfbd37581ca44ee 100644 (file)
@@ -9,6 +9,7 @@
 #include <h/utils.h>
 #include <h/netsec.h>
 #include <stdarg.h>
+#include <sys/time.h>
 #include "h/done.h"
 #include "sbr/base64.h"
 
@@ -39,14 +40,28 @@ DEFINE_SWITCH_ENUM(IMAPTEST);
 DEFINE_SWITCH_ARRAY(IMAPTEST, switches);
 #undef X
 
+struct imap_msg;
+
+struct imap_msg {
+    char *command;             /* Command to send */
+    bool queue;                        /* If true, queue for later delivery */
+    struct imap_msg *next;     /* Next pointer */
+};
+
+struct imap_msg *msgqueue_head = NULL;
+struct imap_msg *msgqueue_tail = NULL;
+
 struct imap_cmd;
 
 struct imap_cmd {
     char tag[16];              /* Command tag */
+    struct timeval start;      /* Time command was sent */
+    char prefix[64];           /* Command prefix */
     struct imap_cmd *next;     /* Next pointer */
 };
 
 static struct imap_cmd *cmdqueue = NULL;
+
 static char *saslmechs = NULL;
 svector_t imap_capabilities = NULL;
 
@@ -56,22 +71,35 @@ static int imap_sasl_callback(enum sasl_message_type, unsigned const char *,
 
 static void parse_capability(const char *, unsigned int len);
 static int capability_set(const char *);
-static int send_imap_command(netsec_context *, char **errstr,
-                            const char *, ...);
-static int get_imap_response(netsec_context *, const char *, char **, char **);
+static void clear_capability(void);
+static int have_capability(void);
+static int send_imap_command(netsec_context *, bool noflush, char **errstr,
+                            const char *fmt, ...) CHECK_PRINTF(4, 5);
+static int get_imap_response(netsec_context *, const char *token,
+                            char **tokenresp, char **status, int failerr,
+                            char **errstr);
+
+static void ts_report(struct timeval *tv, const char *fmt, ...)
+                     CHECK_PRINTF(2, 3);
+
+static void add_msg(bool queue, const char *fmt, ...) CHECK_PRINTF(2, 3);
+
+static bool timestamp = false;
 
 int
 main (int argc, char **argv)
 {
-    bool timestamp = false, sasl = false, tls = false, initialtls = false;
+    bool sasl = false, tls = false, initialtls = false;
     bool snoop = false;
     int fd;
     char *saslmech = NULL, *host = NULL, *port = "143", *user = NULL;
     char *cp, **argp, buf[BUFSIZ], *oauth_svc = NULL, *errstr, **arguments, *p;
     netsec_context *nsc = NULL;
+    struct imap_msg *imsg;
     size_t len;
+    struct timeval tv_start, tv_connect, tv_auth;
 
-    if (nmh_init(argv[0], 1)) { return 1; }
+    if (nmh_init(argv[0], true, true)) { return 1; }
 
     arguments = getarguments (invo_name, argc, argv, 1);
     argp = arguments;
@@ -83,7 +111,7 @@ main (int argc, char **argv)
                ambigsw (cp, switches);
                done (1);
            case UNKWNSW:
-               adios (NULL, "-%s unknown", cp);
+               die("-%s unknown", cp);
 
            case HELPSW:
                snprintf (buf, sizeof(buf), "%s [switches] +folder command "
@@ -96,15 +124,15 @@ main (int argc, char **argv)
 
            case HOSTSW:
                if (!(host = *argp++) || *host == '-')
-                   adios(NULL, "missing argument to %s", argp[-2]);
+                   die("missing argument to %s", argp[-2]);
                continue;
            case PORTSW:
                if (!(port = *argp++) || *port == '-')
-                   adios(NULL, "missing argument to %s", argp[-2]);
+                   die("missing argument to %s", argp[-2]);
                continue;
            case USERSW:
                if (!(user = *argp++) || *user == '-')
-                   adios(NULL, "missing argument to %s", argp[-2]);
+                   die("missing argument to %s", argp[-2]);
                continue;
 
            case SNOOPSW:
@@ -121,11 +149,11 @@ main (int argc, char **argv)
                continue;
            case SASLMECHSW:
                if (!(saslmech = *argp++) || *saslmech == '-')
-                   adios(NULL, "missing argument to %s", argp[-2]);
+                   die("missing argument to %s", argp[-2]);
                continue;
            case AUTHSERVICESW:
                if (!(oauth_svc = *argp++) || *oauth_svc == '-')
-                   adios(NULL, "missing argument to %s", argp[-2]);
+                   die("missing argument to %s", argp[-2]);
                continue;
            case TLSSW:
                tls = true;
@@ -146,11 +174,17 @@ main (int argc, char **argv)
                timestamp = false;
                continue;
            }
+       } else if (*cp == '+') {
+           if (*(cp + 1) == '\0')
+               die("Invalid null folder name");
+           add_msg(0, "SELECT \"%s\"", cp + 1);
+       } else {
+           add_msg(0, "%s", cp);
        }
     }
 
     if (! host)
-       adios(NULL, "A hostname must be given with -host");
+       die("A hostname must be given with -host");
 
     nsc = netsec_init();
 
@@ -164,32 +198,40 @@ main (int argc, char **argv)
 
     if (oauth_svc) {
        if (netsec_set_oauth_service(nsc, oauth_svc) != OK) {
-           adios(NULL, "OAuth2 not supported");
+           die("OAuth2 not supported");
        }
     }
 
+    if (timestamp)
+       gettimeofday(&tv_start, NULL);
+
     if ((fd = client(host, port, buf, sizeof(buf), snoop)) == NOTOK)
-       adios(NULL, "Connect failed: %s", buf);
+       die("Connect failed: %s", buf);
+
+    if (timestamp) {
+       ts_report(&tv_start, "Connect time");
+       gettimeofday(&tv_connect, NULL);
+    }
 
     netsec_set_fd(nsc, fd, fd);
     netsec_set_snoop(nsc, snoop);
 
     if (initialtls || tls) {
        if (netsec_set_tls(nsc, 1, 0, &errstr) != OK)
-           adios(NULL, errstr);
+           die("%s", errstr);
 
        if (initialtls && netsec_negotiate_tls(nsc, &errstr) != OK)
-           adios(NULL, errstr);
+           die("%s", errstr);
     }
 
     if (sasl) {
        if (netsec_set_sasl_params(nsc, "imap", saslmech, imap_sasl_callback,
                                   nsc, &errstr) != OK)
-           adios(NULL, errstr);
+           die("%s", errstr);
     }
 
     if ((cp = netsec_readline(nsc, &len, &errstr)) == NULL) {
-       adios(NULL, errstr);
+       die("%s", errstr);
     }
 
     if (has_prefix(cp, "* BYE")) {
@@ -219,12 +261,13 @@ main (int argc, char **argv)
     } else {
         char *capstring;
 
-       if (send_imap_command(nsc, &errstr, "CAPABILITY") != OK) {
+       if (send_imap_command(nsc, false, &errstr, "CAPABILITY") != OK) {
            fprintf(stderr, "Unable to send CAPABILITY command: %s\n", errstr);
            goto finish;
        }
 
-       if (get_imap_response(nsc, "CAPABILITY", &capstring, &errstr) != OK) {
+       if (get_imap_response(nsc, "CAPABILITY", &capstring, NULL, 1,
+                             &errstr) != OK) {
            fprintf(stderr, "Cannot get CAPABILITY response: %s\n", errstr);
            goto finish;
        }
@@ -238,12 +281,114 @@ main (int argc, char **argv)
        free(capstring);
     }
 
-    send_imap_command(nsc, NULL, "LOGOUT");
-    get_imap_response(nsc, NULL, NULL, NULL);
+    if (tls) {
+       if (!capability_set("STARTTLS")) {
+           fprintf(stderr, "Requested STARTTLS with -tls, but IMAP server "
+                   "has no support for STARTTLS\n");
+           goto finish;
+       }
+       if (send_imap_command(nsc, false, &errstr, "STARTTLS") != OK) {
+           fprintf(stderr, "Unable to issue STARTTLS: %s\n", errstr);
+           goto finish;
+       }
+
+       if (get_imap_response(nsc, NULL, NULL, NULL, 1, &errstr) != OK) {
+           fprintf(stderr, "STARTTLS failed: %s\n", errstr);
+           goto finish;
+       }
+       if (netsec_negotiate_tls(nsc, &errstr) != OK) {
+           die("%s", errstr);
+       }
+    }
+
+    if (sasl) {
+       if (netsec_negotiate_sasl(nsc, saslmechs, &errstr) != OK) {
+           fprintf(stderr, "SASL negotiation failed: %s\n", errstr);
+           goto finish;
+       }
+       /*
+        * Sigh. If we negotiated a SSF AND we got a capability response
+        * as part of the AUTHENTICATE OK message, we can't actually trust
+        * it because it's not protected at that point.  So discard the
+        * capability list and we will generate a fresh CAPABILITY command
+        * later.
+        */
+       if (netsec_get_sasl_ssf(nsc) > 0) {
+           clear_capability();
+       }
+    } else {
+       if (capability_set("LOGINDISABLED")) {
+           fprintf(stderr, "User did not request SASL, but LOGIN "
+                   "is disabled\n");
+           goto finish;
+       }
+    }
+
+    if (!have_capability()) {
+        char *capstring;
+
+       if (send_imap_command(nsc, false, &errstr, "CAPABILITY") != OK) {
+           fprintf(stderr, "Unable to send CAPABILITY command: %s\n", errstr);
+           goto finish;
+       }
+
+       if (get_imap_response(nsc, "CAPABILITY", &capstring, NULL, 1,
+                             &errstr) != OK) {
+           fprintf(stderr, "Cannot get CAPABILITY response: %s\n", errstr);
+           goto finish;
+       }
+
+       if (! capstring) {
+           fprintf(stderr, "No CAPABILITY response seen\n");
+           goto finish;
+       }
+
+       parse_capability(capstring, strlen(capstring));
+       free(capstring);
+    }
+
+    if (timestamp) {
+       ts_report(&tv_connect, "Authentication time");
+       gettimeofday(&tv_auth, NULL);
+    }
+
+    while (msgqueue_head != NULL) {
+       imsg = msgqueue_head;
+
+       if (send_imap_command(nsc, imsg->queue, &errstr, "%s",
+                             imsg->command) != OK) {
+           fprintf(stderr, "Cannot send command \"%s\": %s\n", imsg->command,
+                   errstr);
+           free(errstr);
+           goto finish;
+       }
+
+       if (! imsg->queue) {
+           if (get_imap_response(nsc, NULL, NULL, NULL, 0, &errstr) != OK) {
+               fprintf(stderr, "Unable to get response for command "
+                       "\"%s\": %s\n", imsg->command, errstr);
+               goto finish;
+           }
+       }
+
+       msgqueue_head = imsg->next;
+
+       free(imsg->command);
+       free(imsg);
+    }
+
+    if (timestamp)
+       ts_report(&tv_auth, "Total command execution time");
+
+    send_imap_command(nsc, 0, NULL, "LOGOUT");
+    get_imap_response(nsc, NULL, NULL, NULL, 0, NULL);
 
 finish:
     netsec_shutdown(nsc);
 
+    if (timestamp)
+       ts_report(&tv_start, "Total elapsed time");
+
     exit(0);
 }
 
@@ -276,7 +421,7 @@ parse_capability(const char *cap, unsigned int len)
     }
 
     for (i = 0; caplist[i] != NULL; i++) {
-       if (has_prefix(caplist[i], "AUTH=") && caplist[i] + 5 != '\0') {
+       if (has_prefix(caplist[i], "AUTH=") && *(caplist[i] + 5) != '\0') {
            if (saslmechs) {
                saslmechs = add(" ", saslmechs);
            }
@@ -286,7 +431,6 @@ parse_capability(const char *cap, unsigned int len)
        }
     }
 
-    free(caplist);
     free(str);
 }
 
@@ -303,6 +447,23 @@ capability_set(const char *capability)
     return svector_find(imap_capabilities, capability) != NULL;
 }
 
+/*
+ * Clear the CAPABILITY list (used after we call STARTTLS, for example)
+ */
+
+static void
+clear_capability(void)
+{
+    svector_free(imap_capabilities);
+    imap_capabilities = NULL;
+}
+
+static int
+have_capability(void)
+{
+    return imap_capabilities != NULL;
+}
+
 /*
  * Our SASL callback, which handles the SASL authentication dialog
  */
@@ -314,7 +475,7 @@ imap_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata,
 {
     int rc, snoopoffset;
     char *mech, *line;
-    size_t len, b64len;
+    size_t len;
     netsec_context *nsc = (netsec_context *) context;
 
     switch (mtype) {
@@ -334,16 +495,140 @@ imap_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata,
            b64data = mh_xmalloc(BASE64SIZE(indatalen));
            writeBase64raw(indata, indatalen, (unsigned char *) b64data);
            if (capability_set("SASL-IR")) {
-               rc = send_imap_command(nsc, errstr, "AUTHENTICATE %s %s",
+               netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder,
+                                         &snoopoffset);
+               /*
+                * Sigh. We're assuming a short tag length here.  Really,
+                * I guess we should figure out a way to get the length
+                * of the next tag.
+                */
+               snoopoffset = 17 + strlen(mech);
+               rc = send_imap_command(nsc, 0, errstr, "AUTHENTICATE %s %s",
                                       mech, b64data);
                free(b64data);
+               netsec_set_snoop_callback(nsc, NULL, NULL);
                if (rc)
                    return NOTOK;
            } else {
-               rc = send_imap_command(nsc, errstr, "AUTHENTICATE %s", mech);
+               rc = send_imap_command(nsc, 0, errstr, "AUTHENTICATE %s", mech);
+               line = netsec_readline(nsc, &len, errstr);
+               if (! line)
+                   return NOTOK;
+               /*
+                * We should get a "+ ", nothing else.
+                */
+               if (len != 2 || strcmp(line, "+ ") != 0) {
+                   netsec_err(errstr, "Did not get expected blank response "
+                              "for initial challenge response");
+                   return NOTOK;
+               }
+               rc = netsec_printf(nsc, errstr, "%s\r\n", b64data);
+               free(b64data);
+               if (rc != OK)
+                   return NOTOK;
+               netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, NULL);
+               rc = netsec_flush(nsc, errstr);
+               netsec_set_snoop_callback(nsc, NULL, NULL);
+               if (rc != OK)
+                   return NOTOK;
+           }
+       } else {
+           if (send_imap_command(nsc, 0, errstr, "AUTHENTICATE %s",
+                                 mech) != OK)
+               return NOTOK;
+       }
+
+       break;
+
+    /*
+     * Get a response, decode it and process it.
+     */
+
+    case NETSEC_SASL_READ:
+       netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, &snoopoffset);
+       snoopoffset = 2;
+       line = netsec_readline(nsc, &len, errstr);
+       netsec_set_snoop_callback(nsc, NULL, NULL);
+
+       if (line == NULL)
+           return NOTOK;
+
+       if (len < 2 || (len == 2 && strcmp(line, "+ ") != 0)) {
+           netsec_err(errstr, "Invalid format for SASL response");
+           return NOTOK;
+       }
+
+       if (len == 2) {
+           *outdata = NULL;
+           *outdatalen = 0;
+       } else {
+           rc = decodeBase64(line + 2, outdata, &len, 0, NULL);
+           *outdatalen = len;
+           if (rc != OK) {
+               netsec_err(errstr, "Unable to decode base64 response");
+               return NOTOK;
            }
        }
+       break;
+
+    /*
+     * Simple request encoding
+     */
+
+    case NETSEC_SASL_WRITE:
+       if (indatalen > 0) {
+           unsigned char *b64data;
+           b64data = mh_xmalloc(BASE64SIZE(indatalen));
+           writeBase64raw(indata, indatalen, b64data);
+           rc = netsec_printf(nsc, errstr, "%s", b64data);
+           free(b64data);
+           if (rc != OK)
+               return NOTOK;
+       }
 
+       if (netsec_printf(nsc, errstr, "\r\n") != OK)
+           return NOTOK;
+
+       netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, NULL);
+       rc = netsec_flush(nsc, errstr);
+       netsec_set_snoop_callback(nsc, NULL, NULL);
+       if (rc != OK)
+           return NOTOK;
+       break;
+
+    /*
+     * Finish protocol
+     */
+
+    case NETSEC_SASL_FINISH:
+        line = NULL;
+       if (get_imap_response(nsc, NULL, NULL, &line, 1, errstr) != OK)
+           return NOTOK;
+       /*
+        * We MIGHT get a capability response here.  If so, be sure we
+        * parse it.
+        */
+
+       if (line && has_prefix(line, "OK [CAPABILITY ")) {
+           char *p = line + 15, *q;
+           q = strchr(p, ']');
+
+           if (q)
+               parse_capability(p, q - p);
+       }
+
+       break;
+
+    /*
+     * Cancel an authentication dialog
+     */
+
+    case NETSEC_SASL_CANCEL:
+       rc = netsec_printf(nsc, errstr, "*\r\n");
+       if (rc == OK)
+           rc = netsec_flush(nsc, errstr);
+       if (rc != OK)
+           return NOTOK;
        break;
     }
     return OK;
@@ -354,7 +639,8 @@ imap_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata,
  */
 
 static int
-send_imap_command(netsec_context *nsc, char **errstr, const char *fmt, ...)
+send_imap_command(netsec_context *nsc, bool noflush, char **errstr,
+                 const char *fmt, ...)
 {
     static unsigned int seq = 0;       /* Tag sequence number */
     va_list ap;
@@ -365,6 +651,20 @@ send_imap_command(netsec_context *nsc, char **errstr, const char *fmt, ...)
 
     snprintf(cmd->tag, sizeof(cmd->tag), "A%u ", seq++);
 
+    if (timestamp) {
+       char *p;
+       va_start(ap, fmt);
+       vsnprintf(cmd->prefix, sizeof(cmd->prefix), fmt, ap);
+       va_end(ap);
+
+       p = strchr(cmd->prefix, ' ');
+
+       if (p)
+           *p = '\0';
+
+       gettimeofday(&cmd->start, NULL);
+    }
+
     if (netsec_write(nsc, cmd->tag, strlen(cmd->tag), errstr) != OK) {
        free(cmd);
        return NOTOK;
@@ -382,7 +682,7 @@ send_imap_command(netsec_context *nsc, char **errstr, const char *fmt, ...)
        return NOTOK;
     }
 
-    if (netsec_flush(nsc, errstr) != OK) {
+    if (!noflush && netsec_flush(nsc, errstr) != OK) {
        free(cmd);
        return NOTOK;
     }
@@ -400,10 +700,11 @@ send_imap_command(netsec_context *nsc, char **errstr, const char *fmt, ...)
 
 static int
 get_imap_response(netsec_context *nsc, const char *token, char **tokenresponse,
-                 char **errstr)
+                 char **status, int failerr, char **errstr)
 {
     char *line;
     struct imap_cmd *cmd;
+    bool numerrs = false;
 
     if (tokenresponse)
        *tokenresponse = NULL;
@@ -422,6 +723,9 @@ getline:
 
            if (has_prefix(line, cmdqueue->tag)) {
                cmd = cmdqueue;
+               if (timestamp)
+                   ts_report(&cmd->start, "Command (%s) execution time",
+                             cmd->prefix);
                cmdqueue = cmd->next;
                free(cmd);
            } else {
@@ -429,7 +733,14 @@ getline:
                    if (has_prefix(line, cmd->next->tag)) {
                        struct imap_cmd *cmd2 = cmd->next;
                        cmd->next = cmd->next->next;
+                       if (failerr && strncmp(line + strlen(cmd2->tag),
+                                                       "OK ", 3) != 0) {
+                           numerrs = true;
+                           netsec_err(errstr, "%s", line + strlen(cmd2->tag));
+                       }
                        free(cmd2);
+                       if (status)
+                           *status = getcpy(line);
                        goto getline;
                    }
                    netsec_err(errstr, "Non-matching response line: %s\n",
@@ -440,5 +751,64 @@ getline:
        }
     }
 
-    return OK;
+    return numerrs ? NOTOK : OK;
+}
+
+/*
+ * Add an IMAP command to the msg queue
+ */
+
+static void
+add_msg(bool queue, const char *fmt, ...)
+{
+    struct imap_msg *imsg;
+    va_list ap;
+    size_t msgbufsize;
+    char *msg = NULL;
+    int rc = 63;
+
+    do {
+       msgbufsize = rc + 1;
+       msg = mh_xrealloc(msg, msgbufsize);
+       va_start(ap, fmt);
+       rc = vsnprintf(msg, msgbufsize, fmt, ap);
+       va_end(ap);
+    } while (rc >= (int) msgbufsize);
+
+    imsg = mh_xmalloc(sizeof(*imsg));
+
+    imsg->command = msg;
+    imsg->queue = queue;
+    imsg->next = NULL;
+
+    if (msgqueue_head == NULL) {
+       msgqueue_head = imsg;
+       msgqueue_tail = imsg;
+    } else {
+       msgqueue_tail->next = imsg;
+       msgqueue_tail = imsg;
+    }
+}
+
+/*
+ * Give a timestamp report.
+ */
+
+static void
+ts_report(struct timeval *tv, const char *fmt, ...)
+{
+    struct timeval now;
+    double delta;
+    va_list ap;
+
+    gettimeofday(&now, NULL);
+
+    delta = ((double) now.tv_sec) - ((double) tv->tv_sec) +
+           (now.tv_usec / 1E6 - tv->tv_usec / 1E6);
+
+    va_start(ap, fmt);
+    vfprintf(stdout, fmt, ap);
+    va_end(ap);
+
+    printf(": %f sec\n", delta);
 }