From b39c1b782e06b59f46182ac256273390fa7f87ec Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sun, 26 Jan 2020 14:20:03 -0500 Subject: Implement IMAP flow outline --- notemap.c | 321 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 320 insertions(+), 1 deletion(-) diff --git a/notemap.c b/notemap.c index fbb6e8d..d05f9c0 100644 --- a/notemap.c +++ b/notemap.c @@ -14,15 +14,130 @@ * along with this program. If not, see . */ +#include #include #include #include #include +#include +#include +#include #include +#include +#include #include +#ifndef NO_READPASSPHRASE_H +#include +#endif + +#if !defined(DIG_PATH) && !defined(DRILL_PATH) +# ifdef __FreeBSD__ +# define DRILL_PATH "/usr/bin/drill" +# else +# define DIG_PATH "dig" +# endif +#endif + 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); + + int rw[2]; + int error = pipe(rw); + if (error) err(EX_OSERR, "pipe"); + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + + if (!pid) { + close(rw[0]); + dup2(rw[1], STDOUT_FILENO); + dup2(rw[1], STDERR_FILENO); + close(rw[1]); +#ifdef DRILL_PATH + execlp(DRILL_PATH, DRILL_PATH, buf, "SRV", NULL); + err(EX_CONFIG, "%s", DRILL_PATH); +#else + execlp(DIG_PATH, DIG_PATH, "-t", "SRV", "-q", buf, "+short", NULL); + err(EX_CONFIG, "%s", DIG_PATH); +#endif + } + + int status; + pid = wait(&status); + if (pid < 0) err(EX_OSERR, "wait"); + + close(rw[1]); + FILE *pipe = fdopen(rw[0], "r"); + if (!pipe) err(EX_IOERR, "fdopen"); + + fgets(buf, sizeof(buf), pipe); + if (ferror(pipe)) err(EX_IOERR, "fgets"); + + if (!WIFEXITED(status) || WEXITSTATUS(status)) { + fprintf(stderr, "%s", buf); + exit(WEXITSTATUS(status)); + } + + char *ptr = buf; +#ifdef DRILL_PATH + for (;;) { + char *line = fgets(buf, sizeof(buf), pipe); + if (!line || !strcmp(line, ";; ANSWER SECTION:\n")) break; + } + fgets(buf, sizeof(buf), pipe); + if (ferror(pipe)) err(EX_IOERR, "fgets"); + ptr = strrchr(buf, '\t'); + ptr = (ptr ? ptr + 1 : buf); +#endif + fclose(pipe); + + char *dot = strrchr(ptr, '.'); + if (dot) *dot = '\0'; + strsep(&ptr, " \n"); // priority + strsep(&ptr, " \n"); // weight + *port = strsep(&ptr, " \n"); + *host = strsep(&ptr, " \n"); + if (!*host) { + *host = domain; + *port = "imaps"; + } +} + static const char *uuidGen(void) { byte uuid[16]; arc4random_buf(uuid, sizeof(uuid)); @@ -43,18 +158,90 @@ static const char *uuidGen(void) { return str; } +static bool uuidCheck(const char *uuid) { + if (strlen(uuid) != 36) return false; + if (strspn(uuid, "0123456789abcdef-") != 36) return false; + 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; +} + +static void +append(FILE *imap, const char *from, const char *uuid, const char *path) { +} + int main(int argc, char *argv[]) { const char *path = ".notemap"; bool add = false; + bool force = false; + + const char *user = NULL; + const char *host = NULL; + const char *port = "imaps"; + const char *mailbox = "Notes"; + int rppFlags = 0; int opt; - while (0 < (opt = getopt(argc, argv, "am:"))) { + while (0 < (opt = getopt(argc, argv, "M:afh:m:p:u:vw"))) { switch (opt) { + break; case 'M': mailbox = optarg; break; case 'a': add = true; + break; case 'f': force = true; + break; case 'h': host = optarg; break; case 'm': path = optarg; + break; case 'p': port = optarg; + break; case 'u': user = optarg; + break; case 'v': verbose = true; + break; case 'w': rppFlags |= RPP_STDIN; break; default: return EX_USAGE; } } + if (!user) errx(EX_USAGE, "username required"); + + if (!host) { + const char *domain = strchr(user, '@'); + if (!domain) errx(EX_USAGE, "no domain in username"); + lookup(&host, &port, &domain[1]); + } + + char buf[1024]; + char *pass = readpassphrase( + (rppFlags & RPP_STDIN ? "" : "Password: "), + buf, sizeof(buf), rppFlags + ); + if (!pass) err(EX_UNAVAILABLE, "readpassphrase"); if (add) { FILE *map = fopen(path, "a"); @@ -67,4 +254,136 @@ int main(int argc, char *argv[]) { int error = fclose(map); if (error) err(EX_IOERR, "%s", path); } + + 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; + + 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, " "); + } + 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 : "") + ); + } + + 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: { + ssize_t len; +next: + len = getline(&entry, &entryCap, map); + if (len < 0) err(EX_IOERR, "%s", path); + 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); + } + + // TODO: Skip note if not in argv. + fprintf( + imap, + "%s SEARCH HEADER X-Universally-Unique-Identifier %s\r\n", + Atoms[Search], uuid + ); + } + + break; case Search: { + if (!seq) goto append; + fprintf(imap, "%s FETCH %d INTERNALDATE\r\n", Atoms[Fetch], seq); + } + + break; case Fetch: { +append: + append(imap, user, uuid, note); + } + + break; case Append: { + if (!seq) goto next; + fprintf( + imap, "%s STORE %d +FLAGS.SILENT (\\Deleted)\r\n", + Atoms[Next], seq + ); + } + + break; default:; + } + + if (resp == Search) { + if (rest) { + seq = strtol(rest, &rest, 10); + if (*rest) { + errx(EX_CONFIG, "multiple messages matching %s", uuid); + } + } else { + seq = 0; + } + } + + if (resp == Fetch) { + struct tm date = {0}; + rest = strptime( + rest, "(INTERNALDATE \"%d-%b-%Y %H:%M:%S %z\")", &date + ); + if (!rest) errx(EX_PROTOCOL, "invalid INTERNALDATE"); + + struct stat status; + int error = stat(note, &status); + if (error) err(EX_NOINPUT, "%s", note); + + if (!force && status.st_mtime < mktime(&date)) { + errx(EX_TEMPFAIL, "%s: note has been modified in mailbox", note); + } + } + } } -- cgit 1.4.1