X-Git-Url: https://diplodocus.org/git/nmh/blobdiff_plain/eae4e80fddb67ed353f32a54c9eeed1c1527e5a9..04ee0c3f6ad7e4177cc43a0c57f046013a4fbac9:/uip/imaptest.c diff --git a/uip/imaptest.c b/uip/imaptest.c index e51fe2c5..be276fb1 100644 --- a/uip/imaptest.c +++ b/uip/imaptest.c @@ -5,9 +5,12 @@ * complete copyright information. */ -#include -#include -#include +#include "h/mh.h" +#include "sbr/print_version.h" +#include "sbr/print_help.h" +#include "sbr/error.h" +#include "h/utils.h" +#include "h/netsec.h" #include #include #include "h/done.h" @@ -26,9 +29,14 @@ X("tls", 0, TLSSW) \ X("notls", 0, NOTLSSW) \ X("initialtls", 0, INITIALTLSSW) \ + X("queue", 0, QUEUESW) \ + X("noqueue", 0, NOQUEUESW) \ X("append filename", 0, APPENDSW) \ + X("afolder foldername", 0, AFOLDERSW) \ + X("batch filename", 0, BATCHSW) \ X("timestamp", 0, TIMESTAMPSW) \ X("notimestamp", 0, NOTIMESTAMPSW) \ + X("timeout", 0, TIMEOUTSW) \ X("version", 0, VERSIONSW) \ X("help", 0, HELPSW) \ @@ -44,7 +52,10 @@ struct imap_msg; struct imap_msg { char *command; /* Command to send */ - int queue; /* If true, queue for later delivery */ + char *folder; /* Folder (for append) */ + bool queue; /* If true, queue for later delivery */ + bool append; /* If true, append "command" to mbox */ + size_t msgsize; /* RFC822 size of message */ struct imap_msg *next; /* Next pointer */ }; @@ -73,16 +84,24 @@ static void parse_capability(const char *, unsigned int len); static int capability_set(const char *); static void clear_capability(void); static int have_capability(void); -static int send_imap_command(netsec_context *, int noflush, char **errstr, +static int send_imap_command(netsec_context *, bool noflush, char **errstr, const char *fmt, ...) CHECK_PRINTF(4, 5); +static int send_append(netsec_context *, struct imap_msg *, char **errstr); static int get_imap_response(netsec_context *, const char *token, - char **tokenresp, char **status, int failerr, - char **errstr); + char **tokenresp, char **status, bool contok, + bool failerr, char **errstr); static void ts_report(struct timeval *tv, const char *fmt, ...) CHECK_PRINTF(2, 3); +static void imap_negotiate_tls(netsec_context *); -static void add_msg(int queue, const char *fmt, ...) CHECK_PRINTF(2, 3); +static void add_msg(bool queue, struct imap_msg **, const char *fmt, ...) + CHECK_PRINTF(3, 4); +static void add_append(const char *filename, const char *folder, bool queue); + +static void batchfile(const char *filename, char *afolder, bool queue); + +static size_t rfc822size(const char *filename); static bool timestamp = false; @@ -90,10 +109,11 @@ int main (int argc, char **argv) { bool sasl = false, tls = false, initialtls = false; - bool snoop = false; - int fd; + bool snoop = false, queue = false; + int fd, timeout = 0; char *saslmech = NULL, *host = NULL, *port = "143", *user = NULL; char *cp, **argp, buf[BUFSIZ], *oauth_svc = NULL, *errstr, **arguments, *p; + char *afolder = NULL; netsec_context *nsc = NULL; struct imap_msg *imsg; size_t len; @@ -135,6 +155,31 @@ main (int argc, char **argv) die("missing argument to %s", argp[-2]); continue; + case AFOLDERSW: + if (!(afolder = *argp++) || *afolder == '-') + die("missing argument to %s", argp[-2]); + continue; + case APPENDSW: + if (!*argp || (**argp == '-')) + die("missing argument to %s", argp[-1]); + if (! afolder) + die("Append folder must be set with -afolder first"); + add_append(*argp++, afolder, queue); + continue; + + case BATCHSW: + if (! *argp || (**argp == '-')) + die("missing argument to %s", argp[-1]); + batchfile(*argp++, afolder, queue); + continue; + + case TIMEOUTSW: + if (! *argp || (**argp == '-')) + die("missing argument to %s", argp[-1]); + if (! (timeout = atoi(*argp++))) + die("Invalid timeout: %s", argp[-1]); + continue; + case SNOOPSW: snoop = true; continue; @@ -167,6 +212,12 @@ main (int argc, char **argv) tls = false; initialtls = false; continue; + case QUEUESW: + queue = true; + continue; + case NOQUEUESW: + queue = false; + continue; case TIMESTAMPSW: timestamp = true; continue; @@ -177,9 +228,9 @@ main (int argc, char **argv) } else if (*cp == '+') { if (*(cp + 1) == '\0') die("Invalid null folder name"); - add_msg(0, "SELECT \"%s\"", cp + 1); + add_msg(false, NULL, "SELECT \"%s\"", cp + 1); } else { - add_msg(0, "%s", cp); + add_msg(queue, NULL, "%s", cp); } } @@ -193,6 +244,9 @@ main (int argc, char **argv) netsec_set_hostname(nsc, host); + if (timeout) + netsec_set_timeout(nsc, timeout); + if (snoop) netsec_set_snoop(nsc, 1); @@ -220,8 +274,8 @@ main (int argc, char **argv) if (netsec_set_tls(nsc, 1, 0, &errstr) != OK) die("%s", errstr); - if (initialtls && netsec_negotiate_tls(nsc, &errstr) != OK) - die("%s", errstr); + if (initialtls) + imap_negotiate_tls(nsc); } if (sasl) { @@ -261,12 +315,12 @@ main (int argc, char **argv) } else { char *capstring; - if (send_imap_command(nsc, 0, &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, NULL, 1, + if (get_imap_response(nsc, "CAPABILITY ", &capstring, NULL, false, true, &errstr) != OK) { fprintf(stderr, "Cannot get CAPABILITY response: %s\n", errstr); goto finish; @@ -277,7 +331,9 @@ main (int argc, char **argv) goto finish; } - parse_capability(capstring, strlen(capstring)); + p = capstring + 11; /* "CAPABILITY " */ + + parse_capability(p, strlen(p)); free(capstring); } @@ -287,18 +343,18 @@ main (int argc, char **argv) "has no support for STARTTLS\n"); goto finish; } - if (send_imap_command(nsc, 0, &errstr, "STARTTLS") != OK) { + 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) { + if (get_imap_response(nsc, NULL, NULL, NULL, false, true, + &errstr) != OK) { fprintf(stderr, "STARTTLS failed: %s\n", errstr); goto finish; } - if (netsec_negotiate_tls(nsc, &errstr) != OK) { - die("%s", errstr); - } + + imap_negotiate_tls(nsc); } if (sasl) { @@ -327,13 +383,13 @@ main (int argc, char **argv) if (!have_capability()) { char *capstring; - if (send_imap_command(nsc, 0, &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, NULL, 1, - &errstr) != OK) { + if (get_imap_response(nsc, "CAPABILITY ", &capstring, NULL, false, + true, &errstr) != OK) { fprintf(stderr, "Cannot get CAPABILITY response: %s\n", errstr); goto finish; } @@ -343,7 +399,9 @@ main (int argc, char **argv) goto finish; } - parse_capability(capstring, strlen(capstring)); + p = capstring + 11; /* "CAPABILITY " */ + + parse_capability(p, strlen(p)); free(capstring); } @@ -355,16 +413,24 @@ main (int argc, char **argv) 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->append) { + if (send_append(nsc, imsg, &errstr) != OK) { + fprintf(stderr, "Cannot send APPEND for %s to mbox %s: %s\n", + imsg->command, imsg->folder, errstr); + free(errstr); + goto finish; + } + } else 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) { + if (get_imap_response(nsc, NULL, NULL, NULL, false, false, + &errstr) != OK) { fprintf(stderr, "Unable to get response for command " "\"%s\": %s\n", imsg->command, errstr); goto finish; @@ -374,18 +440,35 @@ main (int argc, char **argv) msgqueue_head = imsg->next; free(imsg->command); + free(imsg->folder); free(imsg); } - ts_report(&tv_auth, "Total command execution time"); + /* + * Flush out any pending network data and get any responses + */ - send_imap_command(nsc, 0, NULL, "LOGOUT"); - get_imap_response(nsc, NULL, NULL, NULL, 0, NULL); + if (netsec_flush(nsc, &errstr) != OK) { + fprintf(stderr, "Error performing final network flush: %s\n", errstr); + free(errstr); + } + + if (get_imap_response(nsc, NULL, NULL, NULL, false, false, &errstr) != OK) { + fprintf(stderr, "Error fetching final command responses: %s\n", errstr); + free(errstr); + } + + if (timestamp) + ts_report(&tv_auth, "Total command execution time"); + + send_imap_command(nsc, false, NULL, "LOGOUT"); + get_imap_response(nsc, NULL, NULL, NULL, false, false, NULL); finish: netsec_shutdown(nsc); - ts_report(&tv_start, "Total elapsed time"); + if (timestamp) + ts_report(&tv_start, "Total elapsed time"); exit(0); } @@ -472,7 +555,7 @@ imap_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata, unsigned int *outdatalen, void *context, char **errstr) { int rc, snoopoffset; - char *mech, *line; + char *mech, *line, *capstring, *p; size_t len; netsec_context *nsc = (netsec_context *) context; @@ -600,15 +683,24 @@ imap_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata, case NETSEC_SASL_FINISH: line = NULL; - if (get_imap_response(nsc, NULL, NULL, &line, 1, errstr) != OK) + if (get_imap_response(nsc, "CAPABILITY ", &capstring, &line, + false, true, errstr) != OK) return NOTOK; /* * We MIGHT get a capability response here. If so, be sure we - * parse it. + * parse it. We ALSO might get a untagged CAPABILITY response. + * Which one should we prefer? I guess we'll go with the untagged + * one. */ - if (line && has_prefix(line, "OK [CAPABILITY ")) { - char *p = line + 15, *q; + if (capstring) { + p = capstring + 11; /* "CAPABILITY " */ + parse_capability(p, strlen(p)); + free(capstring); + } else if (line && has_prefix(line, "OK [CAPABILITY ")) { + char *q; + + p = line + 15; q = strchr(p, ']'); if (q) @@ -637,7 +729,7 @@ imap_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata, */ static int -send_imap_command(netsec_context *nsc, int noflush, char **errstr, +send_imap_command(netsec_context *nsc, bool noflush, char **errstr, const char *fmt, ...) { static unsigned int seq = 0; /* Tag sequence number */ @@ -691,6 +783,116 @@ send_imap_command(netsec_context *nsc, int noflush, char **errstr, return OK; } +/* + * Send an APPEND to the server, which requires some extra semantics + */ + +static int +send_append(netsec_context *nsc, struct imap_msg *imsg, char **errstr) +{ + FILE *f; + bool nonsynlit = false; + char *status = NULL, *line = NULL; + size_t linesize = 0; + ssize_t rc; + + /* + * If we have the LITERAL+ extension, or we have LITERAL- and our + * message is 4096 bytes or less, we can do it all in one message. + * Otherwise we have to wait for a contination marker (+). + */ + + if (capability_set("LITERAL+") || + (capability_set("LITERAL-") && imsg->msgsize <= 4096)) { + nonsynlit = true; + } + + /* + * Send our APPEND command + */ + + if (send_imap_command(nsc, nonsynlit, errstr, "APPEND \"%s\" {%u%s}", + imsg->folder, (unsigned int) imsg->msgsize, + nonsynlit ? "+" : "") != OK) + return NOTOK; + + /* + * If we need to wait for a syncing literal, do that now + */ + + if (! nonsynlit) { + if (get_imap_response(nsc, NULL, NULL, &status, true, + true, errstr) != OK) { + imsg->queue = true; /* XXX Sigh */ + fprintf(stderr, "APPEND command failed: %s\n", *errstr); + free(*errstr); + return OK; + } + if (!(status && has_prefix(status, "+"))) { + netsec_err(errstr, "Expected contination (+), but got: %s", status); + free(status); + return NOTOK; + } + free(status); + } + + /* + * Now write the message out, but make sure we end each line with \r\n + */ + + if ((f = fopen(imsg->command, "r")) == NULL) { + netsec_err(errstr, "Unable to open %s: %s", imsg->command, + strerror(errno)); + return NOTOK; + } + + while ((rc = getline(&line, &linesize, f)) > 0) { + if (rc > 1 && line[rc - 1] == '\n' && line[rc - 2] == '\r') { + if (netsec_write(nsc, line, rc, errstr) != OK) { + free(line); + fclose(f); + return NOTOK; + } + } else { + if (line[rc - 1] == '\n') + rc--; + if (netsec_write(nsc, line, rc, errstr) != OK) { + free(line); + fclose(f); + return NOTOK; + } + if (netsec_write(nsc, "\r\n", 2, errstr) != OK) { + free(line); + fclose(f); + return NOTOK; + } + } + } + + free(line); + + if (! feof(f)) { + netsec_err(errstr, "Error reading %s: %s", imsg->command, + strerror(errno)); + fclose(f); + return NOTOK; + } + + fclose(f); + + /* + * Send a final \r\n for the end of the command + */ + + if (netsec_write(nsc, "\r\n", 2, errstr) != OK) + return NOTOK; + + if (! imsg->queue) + return netsec_flush(nsc, errstr); + + return OK; +} + /* * Get all outstanding responses. If we were passed in a token string * to look for, return it. @@ -698,11 +900,11 @@ send_imap_command(netsec_context *nsc, int noflush, char **errstr, static int get_imap_response(netsec_context *nsc, const char *token, char **tokenresponse, - char **status, int failerr, char **errstr) + char **status, bool condok, bool failerr, char **errstr) { char *line; struct imap_cmd *cmd; - int numerrs = 0; + bool numerrs = false; if (tokenresponse) *tokenresponse = NULL; @@ -717,6 +919,15 @@ getline: free(*tokenresponse); *tokenresponse = getcpy(line + 2); } + } if (condok && has_prefix(line, "+")) { + if (status) { + *status = getcpy(line); + } + /* + * Special case; return now but don't dequeue the tag, + * since we will want to get final result later. + */ + return OK; } else { if (has_prefix(line, cmdqueue->tag)) { @@ -733,23 +944,23 @@ getline: cmd->next = cmd->next->next; if (failerr && strncmp(line + strlen(cmd2->tag), "OK ", 3) != 0) { - numerrs++; + numerrs = true; netsec_err(errstr, "%s", line + strlen(cmd2->tag)); } + if (timestamp) + ts_report(&cmd2->start, "Command (%s) execution " + "time", cmd2->prefix); free(cmd2); if (status) *status = getcpy(line); goto getline; } - netsec_err(errstr, "Non-matching response line: %s\n", - line); - return NOTOK; } } } } - return numerrs == 0 ? OK : NOTOK; + return numerrs ? NOTOK : OK; } /* @@ -757,7 +968,7 @@ getline: */ static void -add_msg(int queue, const char *fmt, ...) +add_msg(bool queue, struct imap_msg **ret_imsg, const char *fmt, ...) { struct imap_msg *imsg; va_list ap; @@ -776,6 +987,8 @@ add_msg(int queue, const char *fmt, ...) imsg = mh_xmalloc(sizeof(*imsg)); imsg->command = msg; + imsg->folder = NULL; + imsg->append = false; imsg->queue = queue; imsg->next = NULL; @@ -786,6 +999,127 @@ add_msg(int queue, const char *fmt, ...) msgqueue_tail->next = imsg; msgqueue_tail = imsg; } + + if (ret_imsg) + *ret_imsg = imsg; +} + +/* + * Add an APPEND command to the queue + */ + +static void +add_append(const char *filename, const char *folder, bool queue) +{ + size_t filesize = rfc822size(filename); + struct imap_msg *imsg; + + add_msg(queue, &imsg, "%s", filename); + + imsg->folder = getcpy(folder); + imsg->append = true; + imsg->msgsize = filesize; +} + +/* + * Process a batch file, which can contain commands (and some arguments) + */ + +static void +batchfile(const char *filename, char *afolder, bool queue) +{ + FILE *f; + char *line = NULL; + size_t linesize = 0; + ssize_t rc; + bool afolder_alloc = false; + + if (!(f = fopen(filename, "r"))) { + die("Unable to open batch file %s: %s", filename, strerror(errno)); + } + + while ((rc = getline(&line, &linesize, f)) > 0) { + line[rc - 1] = '\0'; + if (*line == '-') { + switch (smatch (line + 1, switches)) { + case QUEUESW: + queue = true; + continue; + case NOQUEUESW: + queue = false; + continue; + case AFOLDERSW: + if (afolder_alloc) + free(afolder); + rc = getline(&line, &linesize, f); + if (rc <= 0) + die("Unable to read next line for -afolder"); + if (rc == 1) + die("Folder name cannot be blank"); + line[rc - 1] = '\0'; + afolder = getcpy(line); + afolder_alloc = true; + continue; + + case APPENDSW: + rc = getline(&line, &linesize, f); + if (rc <= 0) + die("Unable to read filename for -append"); + if (rc == 1) + die("Filename for -append cannot be blank"); + line[rc - 1] = '\0'; + add_append(line, afolder, queue); + continue; + + case AMBIGSW: + ambigsw (line, switches); + done (1); + case UNKWNSW: + die("%s unknown", line); + default: + die("Switch %s not supported in batch mode", line); + } + } else if (*line == '+') { + if (*(line + 1) == '\0') + die("Invalid null folder name"); + add_msg(false, NULL, "SELECT \"%s\"", line + 1); + } else { + if (*line == '\0') + continue; /* Ignore blank line */ + add_msg(queue, NULL, "%s", line); + } + } + + if (!feof(f)) { + die("Read of \"%s\" failed: %s", filename, strerror(errno)); + } + + fclose(f); + + if (afolder_alloc) + free(afolder); + + free(line); +} + +/* + * Negotiate TLS connection, with optional timestamp + */ + +static void +imap_negotiate_tls(netsec_context *nsc) +{ + char *errstr; + struct timeval tv; + + if (timestamp) + gettimeofday(&tv, NULL); + + if (netsec_negotiate_tls(nsc, &errstr) != OK) + die("%s", errstr); + + if (timestamp) + ts_report(&tv, "TLS negotation time"); } /* @@ -810,3 +1144,36 @@ ts_report(struct timeval *tv, const char *fmt, ...) printf(": %f sec\n", delta); } + +/* + * Calculate the RFC 822 size of file. + */ + +static size_t +rfc822size(const char *filename) +{ + FILE *f; + size_t total = 0, linecap = 0; + ssize_t rc; + char *line = NULL; + + if (! (f = fopen(filename, "r"))) + die("Unable to open %s: %s", filename, strerror(errno)); + + while ((rc = getline(&line, &linecap, f)) > 0) { + total += rc; + if (line[rc - 1] == '\n' && (rc == 1 || line[rc - 2] != '\r')) + total++; + if (line[rc - 1] != '\n') + total += 2; + } + + free(line); + + if (! feof(f)) + die("Error while reading %s: %s", filename, strerror(errno)); + + fclose(f); + + return total; +}