From 1e24334b33092aa6303e7f3ae3957a0c7fc2240c Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Wed, 8 Apr 2020 19:08:57 -0400 Subject: Use a real IMAP parser --- .gitignore | 1 + Makefile | 10 +- imap.c | 222 ++++++++++++++++++++++++++++++++++++++++++++ imap.h | 130 ++++++++++++++++++++++++++ notemap.c | 304 +++++++++++++++++++++++-------------------------------------- 5 files changed, 474 insertions(+), 193 deletions(-) create mode 100644 imap.c create mode 100644 imap.h diff --git a/.gitignore b/.gitignore index d0b849a..7dbdb7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +*.o config.mk notemap diff --git a/Makefile b/Makefile index 3356cf7..caaa9ac 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,16 @@ LDLIBS = -lcrypto -ltls -include config.mk -notemap: +OBJS += imap.o +OBJS += notemap.o + +notemap: ${OBJS} + ${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@ + +${OBJS}: compat.h imap.h clean: - rm -f notemap + rm -f notemap ${OBJS} install: notemap notemap.1 install -d ${PREFIX}/bin ${MANDIR}/man1 diff --git a/imap.c b/imap.c new file mode 100644 index 0000000..6894319 --- /dev/null +++ b/imap.c @@ -0,0 +1,222 @@ +/* Copyright (C) 2020 C. McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "compat.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "imap.h" + +const char *Atoms[AtomCap] = { +#define X(id, str) [id] = str, + ENUM_ATOM +#undef X +}; + +bool imapVerbose; + +static int imapRead(void *_tls, char *ptr, int len) { + struct tls *tls = _tls; + ssize_t ret; + do { + ret = tls_read(tls, ptr, len); + } while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT); + if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(tls)); + if (imapVerbose) fprintf(stderr, "%.*s", (int)ret, ptr); + return ret; +} + +static int imapWrite(void *_tls, const char *ptr, int len) { + struct tls *tls = _tls; + ssize_t ret; + do { + ret = tls_write(tls, ptr, len); + } while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT); + if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(tls)); + if (imapVerbose) fprintf(stderr, "%.*s", (int)ret, ptr); + return ret; +} + +static int imapClose(void *_tls) { + struct tls *tls = _tls; + int error = tls_close(tls); + if (error) errx(EX_IOERR, "tls_close: %s", tls_error(tls)); + return error; +} + +FILE *imapOpen(const char *host, const char *port) { + struct tls *client = tls_client(); + if (!client) errx(EX_SOFTWARE, "tls_client"); + + struct tls_config *config = tls_config_new(); + if (!config) errx(EX_SOFTWARE, "tls_config_new"); + + int error = tls_configure(client, config); + if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); + tls_config_free(config); + + error = tls_connect(client, host, port); + if (error) errx(EX_NOHOST, "tls_connect: %s", tls_error(client)); + + FILE *imap = funopen(client, imapRead, imapWrite, NULL, imapClose); + if (!imap) err(EX_SOFTWARE, "funopen"); + + setlinebuf(imap); + return imap; +} + +static size_t cap; +static char *buf; +static char *ptr; + +static void imapLine(FILE *imap) { + ssize_t len = getline(&buf, &cap, imap); + if (len < 0) errx(EX_PROTOCOL, "unexpected eof"); + if (len < 1 || buf[len - 1] != '\n') errx(EX_PROTOCOL, "missing LF"); + if (len < 2 || buf[len - 2] != '\r') errx(EX_PROTOCOL, "missing CR"); + buf[len - 2] = '\0'; + ptr = buf; +} + +static struct Data parseAtom(void) { + size_t len = strcspn(ptr, " ()[]{\""); + struct Data data = { + .type = Atom, + .atom = atomn(ptr, len), + }; + ptr += len; + return data; +} + +static struct Data parseNumber(void) { + return (struct Data) { + .type = Number, + .number = strtoull(ptr, &ptr, 10), + }; +} + +static struct Data parseQuoted(void) { + ptr++; + size_t len = strcspn(ptr, "\""); + if (ptr[len] != '"') errx(EX_PROTOCOL, "missing quoted string delimiter"); + struct Data data = { + .type = String, + .string = strndup(ptr, len), + }; + if (!data.string) err(EX_OSERR, "strndup"); + ptr += len + 1; + return data; +} + +static struct Data parseLiteral(FILE *imap) { + ptr++; + size_t len = strtoull(ptr, &ptr, 10); + if (*ptr != '}') errx(EX_PROTOCOL, "missing literal prefix delimiter"); + struct Data data = { + .type = String, + .string = malloc(len + 1), + }; + if (!data.string) err(EX_OSERR, "malloc"); + size_t n = fread(data.string, len, 1, imap); + if (!n) errx(EX_PROTOCOL, "truncated literal"); + imapLine(imap); + data.string[len] = '\0'; + return data; +} + +static struct Data parseData(FILE *imap); + +static struct Data parseList(FILE *imap, char close) { + if (*ptr) ptr++; + struct Data data = { .type = List }; + while (*ptr != close) { + if (data.list.len == data.list.cap) { + if (data.list.cap) { + data.list.cap *= 2; + } else { + data.list.cap = 4; + } + data.list.ptr = realloc( + data.list.ptr, sizeof(*data.list.ptr) * data.list.cap + ); + if (!data.list.ptr) err(EX_OSERR, "realloc"); + } + data.list.ptr[data.list.len++] = parseData(imap); + } + if (*ptr) ptr++; + return data; +} + +static struct Data parseData(FILE *imap) { + if (*ptr == ' ') ptr++; + if (*ptr == '"') return parseQuoted(); + if (*ptr == '{') return parseLiteral(imap); + if (*ptr == '(') return parseList(imap, ')'); + if (*ptr == '[') return parseList(imap, ']'); + if (*ptr >= '0' && *ptr <= '9') return parseNumber(); + if (*ptr) return parseAtom(); + errx(EX_PROTOCOL, "unexpected eof"); +} + +struct Resp imapResp(FILE *imap) { + struct Data data; + struct Resp resp = {0}; + imapLine(imap); + + data = parseData(imap); + if (data.type != Atom) errx(EX_PROTOCOL, "expected tag atom"); + resp.tag = data.atom; + if (resp.tag == AtomContinue) { + if (*ptr == ' ') ptr++; + resp.text = ptr; + return resp; + } + + data = parseData(imap); + if (data.type == Number) { + resp.number = data.number; + data = parseData(imap); + } + if (data.type != Atom) errx(EX_PROTOCOL, "expected response atom"); + resp.resp = data.atom; + + if ( + resp.resp == AtomOk || + resp.resp == AtomNo || + resp.resp == AtomBad || + resp.resp == AtomPreauth || + resp.resp == AtomBye + ) { + if (*ptr == ' ') ptr++; + if (*ptr == '[') { + data = parseList(imap, ']'); + resp.code = data.list; + } + if (*ptr == ' ') ptr++; + resp.text = ptr; + } else { + data = parseList(imap, '\0'); + resp.data = data.list; + } + + return resp; +} diff --git a/imap.h b/imap.h new file mode 100644 index 0000000..04cf5a0 --- /dev/null +++ b/imap.h @@ -0,0 +1,130 @@ +/* Copyright (C) 2020 C. McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#define ENUM_ATOM \ + X(AtomNil, "NIL") \ + X(AtomOk, "OK") \ + X(AtomNo, "NO") \ + X(AtomBad, "BAD") \ + X(AtomPreauth, "PREAUTH") \ + X(AtomBye, "BYE") \ + X(AtomAlert, "ALERT") \ + X(AtomBadCharset, "BADCHARSET") \ + X(AtomCapability, "CAPABILITY") \ + X(AtomParse, "PARSE") \ + X(AtomPermanentFlags, "PERMANENTFLAGS") \ + X(AtomReadOnly, "READ-ONLY") \ + X(AtomReadWrite, "READ-WRITE") \ + X(AtomTryCreate, "TRYCREATE") \ + X(AtomUIDNext, "UIDNEXT") \ + X(AtomUIDValidity, "UIDVALIDITY") \ + X(AtomUnseen, "UNSEEN") \ + X(AtomList, "LIST") \ + X(AtomLSub, "LSUB") \ + X(AtomStatus, "STATUS") \ + X(AtomSearch, "SEARCH") \ + X(AtomFlags, "FLAGS") \ + X(AtomExists, "EXISTS") \ + X(AtomRecent, "RECENT") \ + X(AtomExpunge, "EXPUNGE") \ + X(AtomFetch, "FETCH") \ + X(AtomUntagged, "*") \ + X(AtomContinue, "+") + +enum Atom { +#define X(id, str) id, + ENUM_ATOM +#undef X + AtomCap = 1024, +}; + +extern const char *Atoms[AtomCap]; + +static inline enum Atom atomn(const char *str, size_t len) { + enum Atom i; + for (i = 0; i < AtomCap; ++i) { + if (!Atoms[i]) break; + if (strlen(Atoms[i]) != len) continue; + if (!strncasecmp(Atoms[i], str, len)) return i; + } + if (i == AtomCap) errx(EX_SOFTWARE, "atom capacity exceeded"); + Atoms[i] = strndup(str, len); + if (!Atoms[i]) err(EX_OSERR, "strndup"); + return i; +} + +static inline enum Atom atom(const char *str) { + return atomn(str, strlen(str)); +} + +struct Data { + enum Type { + Atom, + Number, + String, + List, + } type; + union { + enum Atom atom; + uint32_t number; + char *string; + struct List { + size_t cap; + size_t len; + struct Data *ptr; + } list; + }; +}; + +static inline void dataFree(struct Data data) { + if (data.type == String) free(data.string); + if (data.type == List) { + for (size_t i = 0; i < data.list.len; ++i) { + dataFree(data.list.ptr[i]); + } + free(data.list.ptr); + } +} + +struct Resp { + enum Atom tag; + uint32_t number; + enum Atom resp; + struct List code; + struct List data; + const char *text; +}; + +static inline void respFree(struct Resp resp) { + for (size_t i = 0; i < resp.code.len; ++i) { + dataFree(resp.code.ptr[i]); + } + for (size_t i = 0; i < resp.data.len; ++i) { + dataFree(resp.data.ptr[i]); + } +} + +extern bool imapVerbose; +FILE *imapOpen(const char *host, const char *port); +struct Resp imapResp(FILE *imap); diff --git a/notemap.c b/notemap.c index 3d5d853..dfe9872 100644 --- a/notemap.c +++ b/notemap.c @@ -16,9 +16,10 @@ #include "compat.h" -#include #include +#include #include +#include #include #include #include @@ -26,13 +27,14 @@ #include #include #include -#include #include #ifndef NO_READPASSPHRASE_H #include #endif +#include "imap.h" + #if !defined(DIG_PATH) && !defined(DRILL_PATH) # ifdef __FreeBSD__ # define DRILL_PATH "/usr/bin/drill" @@ -43,37 +45,6 @@ typedef unsigned char byte; -static bool verbose; - -int tlsRead(void *_tls, char *ptr, int len) { - struct tls *tls = _tls; - ssize_t ret; - do { - ret = tls_read(tls, ptr, len); - } while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT); - if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(tls)); - if (verbose) fprintf(stderr, "%.*s", (int)ret, ptr); - return ret; -} - -int tlsWrite(void *_tls, const char *ptr, int len) { - struct tls *tls = _tls; - ssize_t ret; - do { - ret = tls_write(tls, ptr, len); - } while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT); - if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(tls)); - if (verbose) fprintf(stderr, "%.*s", (int)ret, ptr); - return ret; -} - -int tlsClose(void *_tls) { - struct tls *tls = _tls; - int error = tls_close(tls); - if (error) errx(EX_IOERR, "tls_close: %s", tls_error(tls)); - return error; -} - static void lookup(const char **host, const char **port, const char *domain) { static char buf[1024]; snprintf(buf, sizeof(buf), "_imaps._tcp.%s", domain); @@ -166,46 +137,9 @@ static bool uuidCheck(const char *uuid) { return true; } -#define ENUM_ATOM \ - X(Unknown, "") \ - X(Untagged, "*") \ - X(Ok, "OK") \ - X(No, "NO") \ - X(Bad, "BAD") \ - X(Bye, "BYE") \ - X(Login, "LOGIN") \ - X(Search, "SEARCH") \ - X(Fetch, "FETCH") \ - X(Append, "APPEND") \ - X(Next, "next") - -enum Atom { -#define X(id, _) id, - ENUM_ATOM -#undef X - AtomsLen, -}; - -static const char *Atoms[AtomsLen] = { -#define X(id, str) [id] = str, - ENUM_ATOM -#undef X -}; - -static enum Atom atom(const char *str) { - if (!str) return Unknown; - for (enum Atom i = 0; i < AtomsLen; ++i) { - if (!strcmp(str, Atoms[i])) return i; - } - return Unknown; -} - #define DATE_FORMAT "%a, %e %b %Y %H:%M:%S %z" -static void append( - FILE *imap, const char *mailbox, - const char *from, const char *uuid, const char *path -) { +static char *format(const char *from, const char *uuid, const char *path) { FILE *note = fopen(path, "r"); if (!note) err(EX_NOINPUT, "%s", path); @@ -277,15 +211,7 @@ static void append( fclose(msg); buf[max - 1] = '\0'; - fprintf( - imap, "%s APPEND %s (\\Seen) {%zu}\r\n", - Atoms[Append], mailbox, strlen(buf) - ); - if (fgetc(imap) == '+') { - ungetc('+', imap); - fprintf(imap, "%s\r\n", buf); - } - free(buf); + return buf; } int main(int argc, char *argv[]) { @@ -309,7 +235,7 @@ int main(int argc, char *argv[]) { break; case 'm': path = optarg; break; case 'p': port = optarg; break; case 'u': user = optarg; - break; case 'v': verbose = true; + break; case 'v': imapVerbose = true; break; case 'w': rppFlags |= RPP_STDIN; break; default: return EX_USAGE; } @@ -346,136 +272,109 @@ int main(int argc, char *argv[]) { FILE *map = fopen(path, "r"); if (!map) err(EX_NOINPUT, "%s", path); - struct tls *client = tls_client(); - if (!client) errx(EX_SOFTWARE, "tls_client"); - - struct tls_config *config = tls_config_new(); - if (!config) errx(EX_SOFTWARE, "tls_config_new"); - - int error = tls_configure(client, config); - if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); - tls_config_free(config); - - error = tls_connect(client, host, port); - if (error) errx(EX_NOHOST, "tls_connect: %s", tls_error(client)); - - FILE *imap = funopen(client, tlsRead, tlsWrite, NULL, tlsClose); - if (!imap) err(EX_SOFTWARE, "funopen"); - setlinebuf(imap); - - char *line = NULL; - size_t lineCap = 0; - + size_t cap = 0; char *entry = NULL; - size_t entryCap = 0; char *uuid = NULL; char *note = NULL; - int seq = 0; - - bool login = false; - while (0 < getline(&line, &lineCap, imap)) { - char *cr = strchr(line, '\r'); - if (cr) *cr = '\0'; - - char *rest = line; - enum Atom tag = atom(strsep(&rest, " ")); - if (rest && isdigit(rest[0])) { - strsep(&rest, " "); + uint32_t seq = 0; + char *message = NULL; + + enum Atom login = 0; + enum Atom next = atom("next"); + enum Atom create = atom("create"); + enum Atom replace = atom("replace"); + enum Atom envelope = atom("ENVELOPE"); + FILE *imap = imapOpen(host, port); + for (struct Resp resp; resp = imapResp(imap), resp.resp != AtomBye;) { + if (resp.resp == AtomNo || resp.resp == AtomBad) { + errx(EX_CONFIG, "%s: %s", Atoms[resp.resp], resp.text); } - enum Atom resp = atom(strsep(&rest, " ")); - if (resp == No || resp == Bad || resp == Bye) { - errx( - EX_CONFIG, "%s: %s %s", - Atoms[tag], Atoms[resp], (rest ? rest : "") + if (!login) { + login = atom("login"); + fprintf( + imap, "%s LOGIN \"%s\" \"%s\"\r\n", + Atoms[login], user, pass ); } - switch (tag) { - break; case Untagged: { - if (login) break; - fprintf( - imap, "%s LOGIN \"%s\" \"%s\"\r\n", - Atoms[Login], user, pass - ); - login = true; - } - - break; case Login: { - fprintf(imap, "%s SELECT \"%s\"\r\n", Atoms[Next], mailbox); - } - - break; case Next: Next: { - ssize_t len; - len = getline(&entry, &entryCap, map); - if (ferror(map)) err(EX_IOERR, "%s", path); - if (len < 1) goto done; - if (entry[len - 1] == '\n') entry[len - 1] = '\0'; - - note = entry; - uuid = strsep(¬e, " "); - if (!note || !uuid || !uuidCheck(uuid)) { - errx(EX_CONFIG, "invalid map entry: %s", entry); - } - - if (argc) { - int i; - for (i = 0; i < argc; ++i) { - if (!argv[i]) continue; - if (strcmp(argv[i], note)) continue; - argv[i] = NULL; - break; - } - if (i == argc) goto Next; - } + if (resp.tag == login) { + fprintf(imap, "%s SELECT \"%s\"\r\n", Atoms[next], mailbox); + } - fprintf( - imap, - "%s SEARCH HEADER X-Universally-Unique-Identifier %s\r\n", - Atoms[Search], uuid - ); + ssize_t len; + if (resp.tag == next) { +next: + len = getline(&entry, &cap, map); + if (ferror(map)) err(EX_IOERR, "%s", path); + if (len < 1) { + fprintf(imap, "ayy LOGOUT\r\n"); + continue; } + if (entry[len - 1] == '\n') entry[len - 1] = '\0'; - break; case Search: { - if (!seq) goto Fetch; - fprintf(imap, "%s FETCH %d ENVELOPE\r\n", Atoms[Fetch], seq); + note = entry; + uuid = strsep(¬e, " "); + if (!note || !uuid || !uuidCheck(uuid)) { + errx(EX_CONFIG, "invalid map entry: %s", entry); } - break; case Fetch: Fetch: { - append(imap, mailbox, user, uuid, note); + if (argc) { + int i; + for (i = 0; i < argc; ++i) { + if (!argv[i]) continue; + if (strcmp(argv[i], note)) continue; + argv[i] = NULL; + break; + } + if (i == argc) goto next; } - break; case Append: { - printf("%c %s\n", (seq ? '~' : '+'), note); - if (!seq) goto Next; - fprintf( - imap, "%s STORE %d +FLAGS.SILENT (\\Deleted)\r\n", - Atoms[Next], seq - ); - } + fprintf( + imap, + "%s SEARCH HEADER X-Universally-Unique-Identifier \"%s\"\r\n", + Atoms[AtomSearch], uuid + ); - break; default:; + respFree(resp); + continue; } - if (resp == Search) { - if (rest) { - seq = strtol(rest, &rest, 10); - if (*rest) { - errx(EX_CONFIG, "multiple messages matching %s", uuid); + if (resp.resp == AtomSearch) { + if (resp.data.len > 1) { + errx(EX_CONFIG, "multiple messages matching %s", uuid); + } + if (resp.data.len) { + if (resp.data.ptr[0].type != Number) { + errx(EX_PROTOCOL, "invalid search result"); } + seq = resp.data.ptr[0].number; + fprintf( + imap, "%s FETCH %" PRIu32 " ENVELOPE\r\n", + Atoms[AtomFetch], seq + ); } else { - seq = 0; + message = format(user, uuid, note); + fprintf( + imap, "%s APPEND %s (\\Seen) {%zu}\r\n", + Atoms[create], mailbox, strlen(message) + ); } } - if (resp == Fetch) { - if (strncmp(rest, "(ENVELOPE", 9)) continue; - + if ( + resp.resp == AtomFetch && + resp.data.len && + resp.data.ptr[0].type == List && + resp.data.ptr[0].list.len > 1 && + resp.data.ptr[0].list.ptr[0].type == Atom && + resp.data.ptr[0].list.ptr[0].atom == envelope && + resp.data.ptr[0].list.ptr[1].type == List + ) { + struct List envelope = resp.data.ptr[0].list.ptr[1].list; struct tm date = {0}; - rest = strptime( - rest, "(ENVELOPE (\"" DATE_FORMAT "\"", &date - ); - if (!rest) errx(EX_PROTOCOL, "invalid envelope date"); + char *rest = strptime(envelope.ptr[0].string, DATE_FORMAT, &date); + if (!rest) errx(EX_PROTOCOL, "invalid envelope date format"); struct stat status; int error = stat(note, &status); @@ -488,13 +387,36 @@ int main(int argc, char *argv[]) { note ); } else if (status.st_mtime == mktime(&date)) { - goto Next; + goto next; } + + message = format(user, uuid, note); + fprintf( + imap, "%s APPEND %s (\\Seen) {%zu}\r\n", + Atoms[replace], mailbox, strlen(message) + ); + } + + if (resp.tag == AtomContinue) { + fprintf(imap, "%s\r\n", message); + free(message); + } + + if (resp.tag == create) { + printf("+ %s\n", note); + goto next; } - } -done: - fprintf(imap, "ayy LOGOUT\r\n"); + if (resp.tag == replace) { + printf("~ %s\n", note); + fprintf( + imap, "%s STORE %" PRIu32 " +FLAGS (\\Deleted)\r\n", + Atoms[next], seq + ); + } + + respFree(resp); + } fclose(imap); int ret = EX_OK; -- cgit 1.4.1