/* 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 #include #include #include #include #include #include "archive.h" #include "imap.h" #define ENV_PASSWORD "BUBGER_IMAP_PASSWORD" static const char *uidPath(uint32_t uid, const char *type) { static char buf[PATH_MAX]; snprintf(buf, sizeof(buf), "UID/%" PRIu32 ".%s", uid, type); return buf; } static void flatten(struct List *flat, struct List nested) { for (size_t i = 0; i < nested.len; ++i) { if (nested.ptr[i].type == List) { flatten(flat, nested.ptr[i].list); } else { listPush(flat, nested.ptr[i]); } } } static enum Atom fetchNew(FILE *imap, struct List threads) { struct List uids = {0}; flatten(&uids, threads); for (size_t i = uids.len - 1; i < uids.len; --i) { if (uids.ptr[i].type != Number) { errx(EX_PROTOCOL, "invalid thread UID"); } uint32_t uid = uids.ptr[i].number; int error = 0; if (!error) error = access(uidPath(uid, "atom"), F_OK); if (!error) error = access(uidPath(uid, "html"), F_OK); if (!error) error = access(uidPath(uid, "mbox"), F_OK); if (!error) { uids.ptr[i] = uids.ptr[--uids.len]; } } enum Atom tag = 0; if (!uids.len) goto done; int error = mkdir("UID", 0775); if (error && errno != EEXIST) err(EX_CANTCREAT, "UID"); tag = atom("fetchNew"); fprintf(imap, "%s UID FETCH ", Atoms[tag]); for (size_t i = 0; i < uids.len; ++i) { fprintf(imap, "%s%" PRIu32, (i ? "," : ""), uids.ptr[i].number); } fprintf( imap, " (" "UID ENVELOPE " "BODY[HEADER.FIELDS (" MBOX_HEADERS ")] BODY[TEXT]" ")\r\n" ); done: listFree(uids); return tag; } static struct Address parseAddress(struct List list) { if (list.len < 4) { errx(EX_PROTOCOL, "missing address structure fields"); } struct Address addr = {0}; if (list.ptr[0].type == String) { // TODO: Decode UTF-8 in name. addr.name = strdup(list.ptr[0].string); if (!addr.name) err(EX_OSERR, "strdup"); } if (list.ptr[2].type == String) addr.mailbox = list.ptr[2].string; if (list.ptr[3].type == String) addr.host = list.ptr[3].string; return addr; } static struct AddressList parseAddressList(struct List list) { struct Address *addrs = calloc(list.len, sizeof(*addrs)); if (!addrs) err(EX_OSERR, "calloc"); for (size_t i = 0; i < list.len; ++i) { if (list.ptr[i].type != List) { errx(EX_PROTOCOL, "invalid address field"); } addrs[i] = parseAddress(list.ptr[i].list); } return (struct AddressList) { list.len, addrs }; } static char *parseID(char *id) { size_t len = strlen(id); if (id[0] != '<' || !len || id[len - 1] != '>') { errx(EX_PROTOCOL, "invalid message ID"); } id[len - 1] = '\0'; return &id[1]; } static struct Envelope parseEnvelope(struct List list) { enum { Date, Subject, From, Sender, ReplyTo, To, Cc, Bcc, InReplyTo, MessageID, EnvelopeLen, }; if (list.len < EnvelopeLen) { errx(EX_PROTOCOL, "missing envelope structure fields"); } struct Envelope envelope = {0}; if (list.ptr[Date].type != String) { errx(EX_PROTOCOL, "invalid envelope date field"); } const char *date = list.ptr[Date].string; date = strptime(date, "%a, %e %b %Y %H:%M:%S %z", &envelope.date); if (!date) errx(EX_PROTOCOL, "invalid envelope date format"); if (list.ptr[Subject].type != String) { errx(EX_PROTOCOL, "invalid envelope subject field"); } // TODO: Decode UTF-8 in subject. envelope.subject = strdup(list.ptr[Subject].string); if (!envelope.subject) err(EX_OSERR, "strdup"); for (size_t i = From; i <= Bcc; ++i) { if (list.ptr[i].type == List) continue; if (list.ptr[i].type == Atom && list.ptr[i].atom == AtomNil) { list.ptr[i].type = List; list.ptr[i].list = (struct List) {0}; continue; } errx(EX_PROTOCOL, "invalid envelope address field"); } for (size_t i = From; i <= ReplyTo; ++i) { if (!list.ptr[i].list.len || list.ptr[i].list.ptr[0].type != List) { errx(EX_PROTOCOL, "invalid envelope address field"); } } envelope.from = parseAddress(list.ptr[From].list.ptr[0].list); envelope.sender = parseAddress(list.ptr[Sender].list.ptr[0].list); envelope.replyTo = parseAddress(list.ptr[ReplyTo].list.ptr[0].list); envelope.to = parseAddressList(list.ptr[To].list); envelope.cc = parseAddressList(list.ptr[Cc].list); envelope.bcc = parseAddressList(list.ptr[Bcc].list); if (list.ptr[InReplyTo].type == String) { envelope.inReplyTo = parseID(list.ptr[InReplyTo].string); } if (list.ptr[MessageID].type != String) { errx(EX_PROTOCOL, "invalid envelope message-id field"); } envelope.messageID = parseID(list.ptr[MessageID].string); return envelope; } static void exportMessage(struct List items) { static enum Atom AtomUID, AtomEnvelope, AtomBody; static enum Atom AtomHeaderFields, AtomText; if (!AtomUID) AtomUID = atom("UID"); if (!AtomEnvelope) AtomEnvelope = atom("ENVELOPE"); if (!AtomBody) AtomBody = atom("BODY"); if (!AtomHeaderFields) AtomHeaderFields = atom("HEADER.FIELDS"); if (!AtomText) AtomText = atom("TEXT"); uint32_t uid = 0; struct Envelope envelope = {0}; char *header = NULL; char *body = NULL; for (size_t i = 0; i + 1 < items.len; i += 2) { enum Atom name; if (items.ptr[i].type == Atom) { name = items.ptr[i].atom; } else if ( items.ptr[i].type == List && items.ptr[i].list.len && items.ptr[i].list.ptr[0].type == Atom ) { name = items.ptr[i].list.ptr[0].atom; } else { errx(EX_PROTOCOL, "invalid data item name"); } if (name == AtomBody) { i--; continue; } else if (name == AtomUID) { if (items.ptr[i + 1].type != Number) { errx(EX_PROTOCOL, "invalid UID data item value"); } uid = items.ptr[i + 1].number; } else if (name == AtomEnvelope) { if (items.ptr[i + 1].type != List) { errx(EX_PROTOCOL, "invalid ENVELOPE data item value"); } envelope = parseEnvelope(items.ptr[i + 1].list); } else if (name == AtomHeaderFields) { if (items.ptr[i + 1].type != String) { errx(EX_PROTOCOL, "invalid BODY[HEADER.FIELDS] data item value"); } header = items.ptr[i + 1].string; } else if (name == AtomText) { if (items.ptr[i + 1].type != String) { errx(EX_PROTOCOL, "invalid BODY[TEXT] data item value"); } body = items.ptr[i + 1].string; } } if (!uid) errx(EX_PROTOCOL, "missing UID data item"); if (!envelope.subject) errx(EX_PROTOCOL, "missing ENVELOPE data item"); if (!header) errx(EX_PROTOCOL, "missing BODY[HEADER.FIELDS] data item"); if (!body) errx(EX_PROTOCOL, "missing BODY[TEXT] data item"); const char *path; FILE *file; int error; path = uidPath(uid, "mbox"); file = fopen(path, "w"); if (!file) err(EX_CANTCREAT, "%s", path); error = mboxFrom(file) || mboxHeader(file, header) || mboxBody(file, body) || fclose(file); if (error) err(EX_IOERR, "%s", path); path = uidPath(uid, "html"); file = fopen(path, "w"); if (!file) err(EX_CANTCREAT, "%s", path); error = htmlEnvelope(file, &envelope) || fclose(file); if (error) err(EX_IOERR, "%s", path); envelopeFree(envelope); } static void checkValidity(uint32_t validity) { FILE *file = fopen("UIDVALIDITY", "r"); if (file) { uint32_t previous; int n = fscanf(file, "%" SCNu32, &previous); if (n < 1) errx(EX_DATAERR, "invalid UIDVALIDITY file"); if (validity != previous) { errx(EX_TEMPFAIL, "UIDVALIDITY changed; fresh export required"); } } else { FILE *file = fopen("UIDVALIDITY", "w"); if (!file) err(EX_CANTCREAT, "UIDVALIDITY"); int n = fprintf(file, "%" PRIu32 "\n", validity); if (n < 0) err(EX_IOERR, "UIDVALIDITY"); int error = fclose(file); if (error) err(EX_IOERR, "UIDVALIDITY"); } } int main(int argc, char *argv[]) { const char *host = NULL; const char *port = "imaps"; const char *user = NULL; const char *passPath = NULL; const char *mailbox = "Archive"; const char *algo = "REFERENCES"; const char *search = "ALL"; const char *title = NULL; const char *headPath = NULL; for (int opt; 0 < (opt = getopt(argc, argv, "C:a:h:p:s:t:vw:"));) { switch (opt) { break; case 'C': { int error = chdir(optarg); if (error) err(EX_NOINPUT, "%s", optarg); } break; case 'a': algo = optarg; break; case 'h': headPath = optarg; break; case 'p': port = optarg; break; case 's': search = optarg; break; case 't': title = optarg; break; case 'v': imapVerbose = true; break; case 'w': passPath = optarg; } } if (optind < argc) host = argv[optind++]; if (optind < argc) user = argv[optind++]; if (optind < argc) mailbox = argv[optind++]; if (!host) errx(EX_USAGE, "host required"); if (!user) errx(EX_USAGE, "user required"); if (!title) title = mailbox; char *pass = NULL; if (passPath) { FILE *file = fopen(passPath, "r"); if (!file) err(EX_NOINPUT, "%s", passPath); size_t cap = 0; ssize_t len = getline(&pass, &cap, file); if (len < 0) err(EX_IOERR, "%s", passPath); if (len && pass[len - 1] == '\n') pass[len - 1] = '\0'; fclose(file); } else { pass = getenv(ENV_PASSWORD); if (!pass) errx(EX_CONFIG, ENV_PASSWORD " unset"); } enum Atom login = 0; enum Atom examine = atom("EXAMINE"); enum Atom thread = atom("THREAD"); enum Atom export = 0; 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); } if (!login) { login = atom("login"); fprintf( imap, "%s LOGIN \"%s\" \"%s\"\r\n", Atoms[login], user, pass ); } if (resp.tag == login) { fprintf(imap, "%s EXAMINE \"%s\"\r\n", Atoms[examine], mailbox); } if ( resp.resp == AtomOk && resp.code.len > 1 && resp.code.ptr[0].type == Atom && resp.code.ptr[0].atom == AtomUIDValidity ) { if (resp.code.ptr[1].type != Number) { errx(EX_PROTOCOL, "invalid UIDVALIDITY"); } checkValidity(resp.code.ptr[1].number); } if (resp.tag == examine) { fprintf( imap, "%s UID THREAD %s UTF-8 %s\r\n", Atoms[thread], algo, search ); } if (resp.resp == thread) { if (!resp.data.len) { errx(EX_TEMPFAIL, "no messages matching %s", search); } export = fetchNew(imap, resp.data); } if (export && resp.resp == AtomFetch) { if (!resp.data.len || resp.data.ptr[0].type != List) { errx(EX_PROTOCOL, "invalid FETCH data"); } exportMessage(resp.data.ptr[0].list); } respFree(resp); } fclose(imap); }