/* 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"); } int main(int argc, char *argv[]) { int exitStatus = 0; bool truncate = false; 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 *search = "ALL"; for ( int opt; 0 < (opt = getopt(argc, argv, "C:H:S:T:a:h:im:p:qs:tu:vw:y:")); ) { switch (opt) { break; case 'C': { int error = chdir(optarg); if (error) err(EX_NOINPUT, "%s", optarg); } break; case 'H': concatHead = optarg; break; case 'S': search = 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 't': truncate = true; 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; 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: if (truncate) { int error = ftruncate(STDOUT_FILENO, 0); if (error) warn("ftruncate"); rewind(stdout); } 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; } struct List threads = {0}; enum Atom thread = atom("thread"); fprintf( imap.w, "%s UID THREAD %s UTF-8 %s\r\n", Atoms[thread], algo, search ); for (; resp = respOk(imapResp(&imap)), resp.tag != thread; respFree(resp)) { if (resp.resp != AtomThread) continue; if (!resp.data.len) { errx(EX_TEMPFAIL, "no messages matching %s", search); } threads = resp.data; resp.data = (struct List) {0}; // prevent freeing threads with resp } respFree(resp); createDirs(); 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); concatIndex(threads, envelopes); fflush(stdout); uidWrite("UIDNEXT", uidNext); for (size_t i = 0; i < threads.len; ++i) { envelopeFree(envelopes[i]); } free(envelopes); listFree(envelopeItems); listFree(threads); 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; }