From edcc7ae1dc9a6e30e9454da7cf0c093f4852b7ea Mon Sep 17 00:00:00 2001 From: Ken Hornstein Date: Tue, 24 Oct 2017 15:32:54 -0400 Subject: [PATCH] It does just what it says on the tin. You should not use this unless you're the sort of person who regularly reads the commit log. Still incomplete for now. --- .gitignore | 1 + Makefile.am | 7 + uip/imaptest.c | 444 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+) create mode 100644 uip/imaptest.c diff --git a/.gitignore b/.gitignore index a3bdb9c0..70a68583 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ a.out.dSYM/ /uip/fmttest /uip/folder /uip/forw +/uip/imaptest /uip/inc /uip/install-mh /uip/mark diff --git a/Makefile.am b/Makefile.am index b75896a1..bd4ee9c1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -293,6 +293,10 @@ bin_PROGRAMS = \ uip/whom \ # +noinst_PROGRAMS = \ + uip/imaptest \ + # + bin_SCRIPTS = \ etc/sendfiles \ uip/mhmail \ @@ -696,6 +700,9 @@ uip_forw_SOURCES = uip/forw.c uip/whatnowproc.c uip/whatnowsbr.c uip/sendsbr.c \ uip/annosbr.c uip/distsbr.c uip/forwsbr.c uip_forw_LDADD = $(LDADD) $(READLINELIB) $(TERMLIB) $(ICONVLIB) $(POSTLINK) +uip_imaptest_SOURCES = uip/imaptest.c +uip_imaptest_LDADD = $(LDADD) $(SASLLIB) $(CURLLIB) $(TLSLIB) $(POSTLINK) + uip_inc_SOURCES = uip/inc.c uip/scansbr.c uip/dropsbr.c uip/popsbr.c uip_inc_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(SASLLIB) $(CURLLIB) \ $(TLSLIB) $(POSTLINK) diff --git a/uip/imaptest.c b/uip/imaptest.c new file mode 100644 index 00000000..0ae9c2db --- /dev/null +++ b/uip/imaptest.c @@ -0,0 +1,444 @@ +/* imaptest.c -- A program to send commands to an IMAP server + * + * This code is Copyright (c) 2017, by the authors of nmh. See the + * COPYRIGHT file in the root directory of the nmh distribution for + * complete copyright information. + */ + +#include +#include +#include +#include +#include "h/done.h" +#include "sbr/base64.h" + +#define IMAPTEST_SWITCHES \ + X("host hostname", 0, HOSTSW) \ + X("user username", 0, USERSW) \ + X("port name/number", 0, PORTSW) \ + X("snoop", 0, SNOOPSW) \ + X("nosnoop", 0, NOSNOOPSW) \ + X("sasl", 0, SASLSW) \ + X("nosasl", 0, NOSASLSW) \ + X("saslmech", 0, SASLMECHSW) \ + X("authservice", 0, AUTHSERVICESW) \ + X("tls", 0, TLSSW) \ + X("notls", 0, NOTLSSW) \ + X("initialtls", 0, INITIALTLSSW) \ + X("append filename", 0, APPENDSW) \ + X("timestamp", 0, TIMESTAMPSW) \ + X("notimestamp", 0, NOTIMESTAMPSW) \ + X("version", 0, VERSIONSW) \ + X("help", 0, HELPSW) \ + +#define X(sw, minchars, id) id, +DEFINE_SWITCH_ENUM(IMAPTEST); +#undef X + +#define X(sw, minchars, id) { sw, minchars, id }, +DEFINE_SWITCH_ARRAY(IMAPTEST, switches); +#undef X + +struct imap_cmd; + +struct imap_cmd { + char tag[16]; /* Command tag */ + struct imap_cmd *next; /* Next pointer */ +}; + +static struct imap_cmd *cmdqueue = NULL; +static char *saslmechs = NULL; +svector_t imap_capabilities = NULL; + +static int imap_sasl_callback(enum sasl_message_type, unsigned const char *, + unsigned int, unsigned char **, unsigned int *, + void *, 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 **); + +int +main (int argc, char **argv) +{ + bool timestamp = false, 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; + size_t len; + + if (nmh_init(argv[0], 1)) { return 1; } + + arguments = getarguments (invo_name, argc, argv, 1); + argp = arguments; + + while ((cp = *argp++)) { + if (*cp == '-') { + switch (smatch (++cp, switches)) { + case AMBIGSW: + ambigsw (cp, switches); + done (1); + case UNKWNSW: + adios (NULL, "-%s unknown", cp); + + case HELPSW: + snprintf (buf, sizeof(buf), "%s [switches] +folder command " + "[command ...]", invo_name); + print_help (buf, switches, 1); + done (0); + case VERSIONSW: + print_version(invo_name); + done (0); + + case HOSTSW: + if (!(host = *argp++) || *host == '-') + adios(NULL, "missing argument to %s", argp[-2]); + continue; + case PORTSW: + if (!(port = *argp++) || *port == '-') + adios(NULL, "missing argument to %s", argp[-2]); + continue; + case USERSW: + if (!(user = *argp++) || *user == '-') + adios(NULL, "missing argument to %s", argp[-2]); + continue; + + case SNOOPSW: + snoop = true; + continue; + case NOSNOOPSW: + snoop = false; + continue; + case SASLSW: + sasl = true; + continue; + case NOSASLSW: + sasl = false; + continue; + case SASLMECHSW: + if (!(saslmech = *argp++) || *saslmech == '-') + adios(NULL, "missing argument to %s", argp[-2]); + continue; + case AUTHSERVICESW: + if (!(oauth_svc = *argp++) || *oauth_svc == '-') + adios(NULL, "missing argument to %s", argp[-2]); + continue; + case TLSSW: + tls = true; + initialtls = false; + continue; + case INITIALTLSSW: + tls = false; + initialtls = true; + continue; + case NOTLSSW: + tls = false; + initialtls = false; + continue; + case TIMESTAMPSW: + timestamp = true; + continue; + case NOTIMESTAMPSW: + timestamp = false; + continue; + } + } + } + + if (! host) + adios(NULL, "A hostname must be given with -host"); + + nsc = netsec_init(); + + if (user) + netsec_set_userid(nsc, user); + + netsec_set_hostname(nsc, host); + + if (snoop) + netsec_set_snoop(nsc, 1); + + if (oauth_svc) { + if (netsec_set_oauth_service(nsc, oauth_svc) != OK) { + adios(NULL, "OAuth2 not supported"); + } + } + + if ((fd = client(host, port, buf, sizeof(buf), snoop)) == NOTOK) + adios(NULL, "Connect failed: %s", buf); + + 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); + + if (initialtls && netsec_negotiate_tls(nsc, &errstr) != OK) + adios(NULL, errstr); + } + + if (sasl) { + if (netsec_set_sasl_params(nsc, "imap", saslmech, imap_sasl_callback, + nsc, &errstr) != OK) + adios(NULL, errstr); + } + + if ((cp = netsec_readline(nsc, &len, &errstr)) == NULL) { + adios(NULL, errstr); + } + + if (has_prefix(cp, "* BYE")) { + fprintf(stderr, "Connection rejected: %s\n", cp + 5); + goto finish; + } + + if (!has_prefix(cp, "* OK") && !has_prefix(cp, "* PREAUTH")) { + fprintf(stderr, "Invalid server response: %s\n", cp); + goto finish; + } + + if ((p = strchr(cp + 2, ' ')) && *(p + 1) != '\0' && + has_prefix(p + 1, "[CAPABILITY ")) { + /* + * Parse the capability list out to the end + */ + char *q; + p += 13; /* 1 + [CAPABILITY + space */ + + if (!(q = strchr(p, ']'))) { + fprintf(stderr, "Cannot find end of CAPABILITY announcement\n"); + goto finish; + } + + parse_capability(p, q - p); + } else { + char *capstring; + + if (send_imap_command(nsc, &errstr, "CAPABILITY") != OK) { + fprintf(stderr, "Unable to send CAPABILITY command: %s\n", errstr); + goto finish; + } + + if (get_imap_response(nsc, "CAPABILITY", &capstring, &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); + } + + send_imap_command(nsc, NULL, "LOGOUT"); + get_imap_response(nsc, NULL, NULL, NULL); + +finish: + netsec_shutdown(nsc); + + exit(0); +} + +/* + * Parse a capability string + * + * Put these into an svector so we can check for stuff later. But special + * things, like AUTH, we parse now + */ + +static void +parse_capability(const char *cap, unsigned int len) +{ + char *str = mh_xmalloc(len + 1); + char **caplist; + int i; + + trunccpy(str, cap, len + 1); + caplist = brkstring(str, " ", NULL); + + if (imap_capabilities) { + svector_free(imap_capabilities); + } + + imap_capabilities = svector_create(32); + + if (saslmechs) { + free(saslmechs); + saslmechs = NULL; + } + + for (i = 0; caplist[i] != NULL; i++) { + if (has_prefix(caplist[i], "AUTH=") && caplist[i] + 5 != '\0') { + if (saslmechs) { + saslmechs = add(" ", saslmechs); + } + saslmechs = add(caplist[i] + 5, saslmechs); + } else { + svector_push_back(imap_capabilities, getcpy(caplist[i])); + } + } + + free(caplist); + free(str); +} + +/* + * Return 1 if a particular capability is set + */ + +static int +capability_set(const char *capability) +{ + if (!imap_capabilities) + return 0; + + return svector_find(imap_capabilities, capability) != NULL; +} + +/* + * Our SASL callback, which handles the SASL authentication dialog + */ + +static int +imap_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata, + unsigned int indatalen, unsigned char **outdata, + unsigned int *outdatalen, void *context, char **errstr) +{ + int rc, snoopoffset; + char *mech, *line; + size_t len, b64len; + netsec_context *nsc = (netsec_context *) context; + + switch (mtype) { + case NETSEC_SASL_START: + /* + * Generate our AUTHENTICATE message. + * + * If SASL-IR capability is set, we can include any initial response + * in the AUTHENTICATE command. Otherwise we have to wait for + * the first server response (which should be blank). + */ + + mech = netsec_get_sasl_mechanism(nsc); + + if (indatalen) { + char *b64data; + 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", + mech, b64data); + free(b64data); + if (rc) + return NOTOK; + } else { + rc = send_imap_command(nsc, errstr, "AUTHENTICATE %s", mech); + } + } + + break; + } + return OK; +} + +/* + * Send a single command to the IMAP server. + */ + +static int +send_imap_command(netsec_context *nsc, char **errstr, const char *fmt, ...) +{ + static unsigned int seq = 0; /* Tag sequence number */ + va_list ap; + int rc; + struct imap_cmd *cmd; + + cmd = mh_xmalloc(sizeof(*cmd)); + + snprintf(cmd->tag, sizeof(cmd->tag), "A%u ", seq++); + + if (netsec_write(nsc, cmd->tag, strlen(cmd->tag), errstr) != OK) { + free(cmd); + return NOTOK; + } + + va_start(ap, fmt); + rc = netsec_vprintf(nsc, errstr, fmt, ap); + va_end(ap); + + if (rc == OK) + rc = netsec_write(nsc, "\r\n", 2, errstr); + + if (rc != OK) { + free(cmd); + return NOTOK; + } + + if (netsec_flush(nsc, errstr) != OK) { + free(cmd); + return NOTOK; + } + + cmd->next = cmdqueue; + cmdqueue = cmd; + + return OK; +} + +/* + * Get all outstanding responses. If we were passed in a token string + * to look for, return it. + */ + +static int +get_imap_response(netsec_context *nsc, const char *token, char **tokenresponse, + char **errstr) +{ + char *line; + struct imap_cmd *cmd; + + if (tokenresponse) + *tokenresponse = NULL; + +getline: + while (cmdqueue) { + if (!(line = netsec_readline(nsc, NULL, errstr))) + return NOTOK; + if (has_prefix(line, "* ") && *(line + 2) != '\0') { + if (token && tokenresponse && has_prefix(line + 2, token)) { + if (*tokenresponse) + free(*tokenresponse); + *tokenresponse = getcpy(line + 2); + } + } else { + + if (has_prefix(line, cmdqueue->tag)) { + cmd = cmdqueue; + cmdqueue = cmd->next; + free(cmd); + } else { + for (cmd = cmdqueue; cmd->next != NULL; cmd = cmd->next) { + if (has_prefix(line, cmd->next->tag)) { + struct imap_cmd *cmd2 = cmd->next; + cmd->next = cmd->next->next; + free(cmd2); + goto getline; + } + netsec_err(errstr, "Non-matching response line: %s\n", + line); + return NOTOK; + } + } + } + } + + return OK; +} -- 2.56.0