From 6dccf2dcc13cc235547d6f35ef30cee77b8d2569 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sat, 21 Dec 2019 06:25:06 -0500 Subject: Add first working version --- imbox.1 | 90 +++++++++++++++++++ imbox.c | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 396 insertions(+) create mode 100644 imbox.1 create mode 100644 imbox.c diff --git a/imbox.1 b/imbox.1 new file mode 100644 index 0000000..791c996 --- /dev/null +++ b/imbox.1 @@ -0,0 +1,90 @@ +.Dd December 20, 2019 +.Dt IMBOX 1 +.Os +. +.Sh NAME +.Nm imbox +.Nd IMAP to mboxrd +. +.Sh SYNOPSIS +.Nm +.Op Fl vw +.Op Fl h Ar host +.Op Fl m Ar mailbox +.Op Fl p Ar port +.Op Fl s Ar search +.Ar user +. +.Sh DESCRIPTION +The +.Nm +utility exports messages from an IMAP mailbox +to the mboxrd format on standard output. +IMAP login is performed as +.Ar user +with a password read from +.Pa /dev/tty , +or standard input if +.Fl w +is used. +. +.Pp +The arguments are as follows: +.Bl -tag -width Ds +.It Fl h Ar host +Connect to +.Ar host . +The default host is +.Li imap.fastmail.com . +TLS without STARTTLS is assumed. +. +.It Fl m Ar mailbox +Export messages from +.Ar mailbox . +The default mailbox is INBOX. +. +.It Fl p Ar port +Connect to +.Ar port . +The default port is 993. +. +.It Fl s Ar search +Export messages matching +.Ar search . +The default search is +.Ql SUBJECT \(dq[PATCH\(dq . +. +.It Fl v +Log IMAP protocol to standard error. +. +.It Fl w +Read the IMAP password from standard input. +.El +. +.Sh EXAMPLES +.Bd -literal +imbox june@causal.agency | git am +.Ed +. +.Sh STANDARDS +.Bl -item +.It +.Rs +.%A M. Crispin +.%Q University of Washington +.%T INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1 +.%I IETF +.%N RFC 3501 +.%D March 2003 +.%U https://tools.ietf.org/html/rfc3501 +.Re +.It +.Rs +.%A E. Hall +.%T The application/mbox Media Type +.%I IETF +.%N RFC 4155 +.%D September 2005 +.%U https://tools.ietf.org/html/rfc4155 +.Re +.El diff --git a/imbox.c b/imbox.c new file mode 100644 index 0000000..9f46983 --- /dev/null +++ b/imbox.c @@ -0,0 +1,306 @@ +/* Copyright (C) 2019 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 +#include +#include +#include +#include + +#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) + +static void compile(regex_t *regex, const char *pattern) { + if (regex->re_nsub) return; + int error = regcomp(regex, pattern, REG_EXTENDED | REG_NEWLINE); + if (!error) return; + char buf[256]; + regerror(error, regex, buf, sizeof(buf)); + errx(EX_SOFTWARE, "regcomp: %s: %s", buf, pattern); +} + +static void mboxrd(const char *headers, const char *body) { + static regex_t fromRegex; + compile(&fromRegex, "^From: .*<([^>]+)>"); + + regmatch_t from[2]; + int error = regexec(&fromRegex, headers, 2, from, 0); + if (error) errx(EX_DATAERR, "missing From header"); + printf( + "From %.*s ", + (int)(from[1].rm_eo - from[1].rm_so), &headers[from[1].rm_so] + ); + + static regex_t dateRegex; + compile(&dateRegex, "^Date: (...), (..) (...) (....) (.{8})"); + + regmatch_t date[6]; + error = regexec(&dateRegex, headers, 6, date, 0); + if (error) errx(EX_DATAERR, "missing Date header"); + printf( + "%.*s %.*s %.*s %.*s %.*s\r\n", + (int)(date[1].rm_eo - date[1].rm_so), &headers[date[1].rm_so], + (int)(date[3].rm_eo - date[3].rm_so), &headers[date[3].rm_so], + (int)(date[2].rm_eo - date[2].rm_so), &headers[date[2].rm_so], + (int)(date[5].rm_eo - date[5].rm_so), &headers[date[5].rm_so], + (int)(date[4].rm_eo - date[4].rm_so), &headers[date[4].rm_so] + ); + + printf("%s", headers); + // FIXME: There seems to sometimes be some garbage data at the end of the + // headers? + + static regex_t quoteRegex; + compile("eRegex, "^>*From "); + regmatch_t match = {0}; + for (const char *ptr = body;; ptr += match.rm_eo) { + regmatch_t match; + error = regexec( + "eRegex, body, 1, &match, (ptr > body ? REG_NOTBOL : 0) + ); + if (error) { + printf("%s", ptr); + break; + } + printf( + "%.*s>%.*s", + (int)match.rm_so, ptr, + (int)(match.rm_eo - match.rm_so), &ptr[match.rm_so] + ); + } + + // FIXME: mbox technically shouldn't use \r\n but everything else does... + printf("\r\n"); +} + +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; +} + +#define ENUM_ATOM \ + X(Unknown, "") \ + X(Untagged, "*") \ + X(Ok, "OK") \ + X(No, "NO") \ + X(Bad, "BAD") \ + X(Bye, "BYE") \ + X(Login, "LOGIN") \ + X(Select, "SELECT") \ + X(Search, "SEARCH") \ + X(Fetch, "FETCH") + +enum Atom { +#define X(id, _) id, + ENUM_ATOM +#undef X +}; + +static const char *Atoms[] = { +#define X(id, str) [id] = str, + ENUM_ATOM +#undef X +}; + +static enum Atom atom(const char *str) { + if (!str) return Unknown; + for (size_t i = 0; i < ARRAY_LEN(Atoms); ++i) { + if (!strcmp(str, Atoms[i])) return i; + } + return Unknown; +} + +static char *readLiteral(FILE *imap, const char *line) { + char *prefix = strrchr(line, '{'); + if (!prefix) errx(EX_PROTOCOL, "no literal prefix"); + + size_t size = strtoul(prefix + 1, NULL, 10); + if (!size) errx(EX_PROTOCOL, "invalid literal size"); + + char *literal = malloc(size); + if (!literal) err(EX_OSERR, "malloc"); + + size_t count = fread(literal, size, 1, imap); + if (!count) errx(EX_PROTOCOL, "could not read literal"); + + return literal; +} + +int main(int argc, char *argv[]) { + const char *host = "imap.fastmail.com"; + const char *port = "993"; + const char *mailbox = "INBOX"; + const char *search = "SUBJECT \"[PATCH\""; + int rppFlags = 0; + + int opt; + while (0 < (opt = getopt(argc, argv, "h:m:p:s:vw"))) { + switch (opt) { + break; case 'h': host = optarg; + break; case 'm': mailbox = optarg; + break; case 'p': port = optarg; + break; case 's': search = optarg; + break; case 'v': verbose = true; + break; case 'w': rppFlags |= RPP_STDIN; + break; default: return EX_USAGE; + } + } + + const char *user = argv[optind]; + if (!user) errx(EX_USAGE, "username required"); + + char buf[1024]; + char *pass = readpassphrase( + (rppFlags & RPP_STDIN ? "" : "Password: "), + buf, sizeof(buf), rppFlags + ); + if (!pass) err(EX_UNAVAILABLE, "readpassphrase"); + + 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"); + + bool login = false; + char *uids = NULL; + + char *line = NULL; + size_t cap = 0; + while (0 < getline(&line, &cap, imap)) { + if (strchr(line, '\r')) *strchr(line, '\r') = '\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) { + errx( + EX_CONFIG, "%s: %s %s", + Atoms[tag], Atoms[resp], (rest ? rest : "") + ); + } else if (resp == Bye) { + errx(EX_UNAVAILABLE, "unexpected BYE: %s", (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[Select], mailbox); + } + break; case Select: { + fprintf( + imap, "%s UID SEARCH CHARSET UTF-8 %s\r\n", + Atoms[Search], search + ); + } + break; case Search: { + if (!uids) errx(EX_PROTOCOL, "no search response"); + for (char *ch = uids; *ch; ++ch) { + if (*ch == ' ') *ch = ','; + } + // FIXME: Grab Content-Encoding as well? + fprintf( + imap, + "%s UID FETCH %s (" + "BODY[HEADER.FIELDS (Date From Subject Message-Id)] " + "BODY[TEXT]" + ")\r\n", + Atoms[Fetch], uids + ); + } + break; case Fetch: { + fprintf(imap, "ayy LOGOUT\r\n"); + fclose(imap); + return EX_OK; + } + break; default:; + } + + switch (resp) { + break; case Search: { + if (!rest) errx(EX_TEMPFAIL, "no messages match"); + uids = strdup(rest); + if (!uids) err(EX_OSERR, "strdup"); + } + break; case Fetch: { + char *headers = readLiteral(imap, rest); + + ssize_t len = getline(&line, &cap, imap); + if (len <= 0) errx(EX_PROTOCOL, "unexpected eof"); + + char *body = readLiteral(imap, line); + + len = getline(&line, &cap, imap); + if (len <= 0) errx(EX_PROTOCOL, "unexpected eof"); + if (strcmp(line, ")\r\n")) { + errx(EX_PROTOCOL, "trailing fetch data"); + } + + mboxrd(headers, body); + free(headers); + free(body); + } + break; default:; + } + } +} -- cgit 1.4.1