From: Ken Hornstein Date: Tue, 24 Oct 2017 19:32:54 +0000 (-0400) Subject: It does just what it says on the tin. X-Git-Url: https://diplodocus.org/git/nmh/commitdiff_plain/edcc7ae1dc9a6e30e9454da7cf0c093f4852b7ea 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. --- 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; +}