/* 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 . * * Additional permission under GNU GPL version 3 section 7: * * If you modify this Program, or any covered work, by linking or * combining it with OpenSSL (or a modified version of that library), * containing parts covered by the terms of the OpenSSL License and the * original SSLeay license, the licensors of this Program grant you * additional permission to convey the resulting work. Corresponding * Source for a non-source form of such a combination shall include the * source code for the parts of OpenSSL used as well as that of the * covered work. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "archive.h" #include "imap.h" #define ENV_PASSWORD "BUBGER_IMAP_PASSWORD" bool quiet; const char *baseURL = ""; const char *baseTitle; const char *baseMailto; const char *baseSubscribe; const char *baseStylesheet; static uint32_t uidRead(const char *path) { FILE *file = fopen(path, "r"); if (!file) return 0; uint32_t uid; int n = fscanf(file, "%" SCNu32, &uid); if (n < 1) errx(EX_DATAERR, "%s: invalid UID", path); return uid; } static void uidWrite(const char *path, uint32_t uid) { FILE *file = fopen(path, "w"); if (!file) err(EX_CANTCREAT, "%s", path); int n = fprintf(file, "%" PRIu32 "\n", uid); if (n < 0) err(EX_IOERR, "%s", path); int error = fclose(file); if (error) err(EX_IOERR, "%s", path); } static void createDir(const char *path) { int error = mkdir(path, 0775); if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", path); } static void createDirs(void) { createDir("UID"); createDir("attachment"); createDir("message"); createDir("thread"); } struct Search search; static void searchAdd(const char *name, const char *expr) { if (search.len == search.cap) { search.cap = (search.cap ? search.cap * 2 : 8); search.names = realloc( search.names, sizeof(*search.names) * search.cap ); search.exprs = realloc( search.exprs, sizeof(*search.exprs) * search.cap ); if (!search.names || !search.exprs) err(EX_OSERR, "realloc"); } size_t i = search.len++; search.names[i] = strdup(name); search.exprs[i] = strdup(expr); if (!search.names[i] || !search.exprs[i]) err(EX_OSERR, "strdup"); } static int searchRead(const char *path) { FILE *file = fopen(path, "r"); if (!file) return -1; int line = 1; size_t cap = 0; char *buf = NULL; for (ssize_t len; 0 < (len = getline(&buf, &cap, file)); ++line) { if (buf[len - 1] == '\n') buf[len - 1] = '\0'; if (!buf[0] || buf[0] == '#') continue; char *expr = buf; char *name = strsep(&expr, " \t"); if (!expr) { errx(EX_DATAERR, "%s:%d: missing search expression", path, line); } searchAdd(name, &expr[strspn(expr, " \t")]); } if (ferror(file)) err(EX_IOERR, "%s", path); fclose(file); return 0; } static void searchDefault(void) { for (size_t i = 0; i < search.len; ++i) { if (!strcmp(search.names[i], "index")) return; } searchAdd("index", "ALL"); } static void searchThreads( struct IMAP *imap, struct List threads, const struct Envelope *envelopes, const char *name, const char *expr ) { struct Resp resp; struct List roots = {0}; if (!strcmp(expr, "ALL")) { for (size_t i = 0; i < threads.len; ++i) { struct Data root = { .type = Number, .number = threadRoot(dataCheck(threads.ptr[i], List).list) }; listPush(&roots, root); } goto concat; } enum Atom search = atom("search"); concatSearch(imap->w, search, threads, expr); for (; resp = respOk(imapResp(imap)), resp.tag != search; respFree(resp)) { if (resp.resp != AtomSearch) continue; roots = resp.data; resp.data = (struct List) {0}; // prevent freeing roots with resp } respFree(resp); concat: concatIndex(name, roots, threads, envelopes); listFree(roots); } int main(int argc, char *argv[]) { int exitStatus = 0; const char *host = NULL; const char *port = "imaps"; const char *user = NULL; const char *passPath = NULL; bool idle = false; const char *mailbox = "Archive"; const char *algo = "REFERENCES"; const char *searchPath = NULL; for ( int opt; 0 < (opt = getopt(argc, argv, "A:C:H:S:T:a:h:im:p:qs:u:vw:y:")); ) { switch (opt) { break; case 'A': concatIndexEntries = strtoul(optarg, NULL, 10); break; case 'C': { int error = chdir(optarg); if (error) err(EX_NOINPUT, "%s", optarg); } break; case 'H': concatHead = optarg; break; case 'S': searchPath = optarg; break; case 'T': baseTitle = optarg; break; case 'a': algo = optarg; break; case 'h': host = optarg; break; case 'i': idle = true; break; case 'm': baseMailto = optarg; break; case 'p': port = optarg; break; case 'q': quiet = true; exitStatus = EXIT_FAILURE; break; case 's': baseSubscribe = optarg; break; case 'u': baseURL = optarg; break; case 'v': imapVerbose = true; break; case 'w': passPath = optarg; break; case 'y': baseStylesheet = optarg; break; default: return EX_USAGE; } } if (optind < argc) user = argv[optind++]; if (optind < argc) mailbox = argv[optind++]; if (!user) errx(EX_USAGE, "user required"); if (!host) { host = strchr(user, '@'); if (!host) errx(EX_USAGE, "host required"); host++; } if (!baseTitle) baseTitle = mailbox; if (searchPath) { int error = searchRead(searchPath); if (error) err(EX_NOINPUT, "%s", searchPath); } else { searchRead("SEARCH"); } searchDefault(); 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"); } #ifdef __OpenBSD__ struct { const char *path; const char *perm; } paths[] = { { ".", "rwc" }, { tls_default_ca_cert_file(), "r" }, { concatHead, "r" }, {0}, }; for (int i = 0; paths[i].path; ++i) { int error = unveil(paths[i].path, paths[i].perm); if (error) err(EX_NOINPUT, "%s", paths[i].path); } int error = pledge("stdio rpath wpath cpath inet dns", NULL); if (error) err(EX_OSERR, "pledge"); #endif struct IMAP imap = imapOpen(host, port); #ifdef __OpenBSD__ error = pledge("stdio rpath wpath cpath", NULL); if (error) err(EX_OSERR, "pledge"); #endif struct Resp resp; respFree(respOk(imapResp(&imap))); enum Atom login = atom("login"); fprintf(imap.w, "%s LOGIN \"%s\" \"%s\"\r\n", Atoms[login], user, pass); for (; resp = respOk(imapResp(&imap)), resp.tag != login; respFree(resp)); respFree(resp); examine:; uint32_t uidNext = 0; uint32_t uidValidity = 0; enum Atom examine = atom("examine"); fprintf(imap.w, "%s EXAMINE \"%s\"\r\n", Atoms[examine], mailbox); for (; resp = respOk(imapResp(&imap)), resp.tag != examine; respFree(resp)) { if (resp.resp != AtomOk || resp.code.len < 2) continue; enum Atom code = dataCheck(resp.code.ptr[0], Atom).atom; if (code == AtomUIDValidity) { uidValidity = dataCheck(resp.code.ptr[1], Number).number; } else if (code == AtomUIDNext) { uidNext = dataCheck(resp.code.ptr[1], Number).number; } } respFree(resp); uint32_t previous = uidRead("UIDVALIDITY"); if (previous && uidValidity != previous) { errx(EX_TEMPFAIL, "UIDVALIDITY changed; fresh export required"); } if (!previous) uidWrite("UIDVALIDITY", uidValidity); previous = uidRead("UIDNEXT"); if (uidNext != previous) { exitStatus = EXIT_SUCCESS; } else if (idle) { goto idle; } else { goto logout; } createDirs(); struct List threads = {0}; enum Atom thread = atom("thread"); fprintf(imap.w, "%s UID THREAD %s UTF-8 ALL\r\n", Atoms[thread], algo); for (; resp = respOk(imapResp(&imap)), resp.tag != thread; respFree(resp)) { if (resp.resp != AtomThread) continue; threads = resp.data; resp.data = (struct List) {0}; // prevent freeing threads with resp } respFree(resp); enum Atom export = atom("export"); if (!exportFetch(imap.w, export, threads)) { goto concat; } for ( size_t exportTags = 1; resp = respOk(imapResp(&imap)), resp.tag != export || --exportTags; respFree(resp) ) { if (resp.resp != AtomFetch) continue; if (!resp.data.len) errx(EX_PROTOCOL, "missing FETCH data"); struct List items = dataCheck(resp.data.ptr[0], List).list; if (exportData(imap.w, export, items)) exportTags++; } respFree(resp); concat:; enum Atom concat = atom("concat"); struct List envelopeItems = {0}; struct Envelope *envelopes = calloc(threads.len, sizeof(*envelopes)); if (!envelopes) err(EX_OSERR, "calloc"); concatFetch(imap.w, concat, threads); for (; resp = respOk(imapResp(&imap)), resp.tag != concat; respFree(resp)) { if (resp.resp != AtomFetch) continue; if (!resp.data.len) errx(EX_PROTOCOL, "missing FETCH data"); // Prevent freeing data in envelopes with resp: struct Data items = dataTake(&resp.data.ptr[0]); concatData(threads, envelopes, dataCheck(items, List).list); listPush(&envelopeItems, items); } respFree(resp); concatThreads(threads, envelopes); for (size_t i = 0; i < search.len; ++i) { searchThreads( &imap, threads, envelopes, search.names[i], search.exprs[i] ); } for (size_t i = 0; i < threads.len; ++i) { envelopeFree(envelopes[i]); } free(envelopes); listFree(envelopeItems); listFree(threads); fflush(stdout); uidWrite("UIDNEXT", uidNext); if (!idle) goto logout; idle: respFree(respOk(imapIdle(&imap, atom("idle")))); goto examine; logout: fprintf(imap.w, "ayy LOGOUT\r\n"); fclose(imap.r); fclose(imap.w); return exitStatus; }