/* 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 LibreSSL (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 LibreSSL used as well as that of the * covered work. */ #include #include #include #include #include #include #include #include #include #include "archive.h" #include "imap.h" #define ENV_PASSWORD "BUBGER_IMAP_PASSWORD" const char *baseURL = ""; const char *baseTitle; const char *baseMailto; const char *baseSubscribe; 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; 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"; for (int opt; 0 < (opt = getopt(argc, argv, "C:S:a:h:m:p:qs:t:u:vw:"));) { switch (opt) { break; case 'C': { int error = chdir(optarg); if (error) err(EX_NOINPUT, "%s", optarg); } break; case 'S': search = optarg; break; case 'a': algo = optarg; break; case 'h': concatHead = optarg; break; case 'm': baseMailto = optarg; break; case 'p': port = optarg; break; case 'q': exitStatus = EXIT_FAILURE; break; case 's': baseSubscribe = optarg; break; case 't': baseTitle = optarg; break; case 'u': baseURL = 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 (!baseTitle) baseTitle = mailbox; if (!baseMailto) baseMailto = user; 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 { Ready, Login, Examine, Thread, Export, Concat, Logout, } state = Ready; size_t exportTags = 0; enum Atom login = atom("login"); enum Atom examine = atom("examine"); enum Atom thread = atom("thread"); enum Atom export = atom("export"); enum Atom concat = atom("concat"); uint32_t uidNext = 0; struct List threads = {0}; struct Envelope *envelopes = NULL; FILE *imapRead, *imap; imapOpen(&imapRead, &imap, host, port); for ( struct Resp resp; resp = imapResp(imapRead), resp.resp != AtomBye; respFree(resp) ) { if (resp.resp == AtomNo || resp.resp == AtomBad) { errx(EX_CONFIG, "%s: %s", Atoms[resp.tag], resp.text); } switch (state) { break; case Ready: { fprintf( imap, "%s LOGIN \"%s\" \"%s\"\r\n", Atoms[login], user, pass ); state = Login; } break; case Login: { if (resp.tag != login) break; fprintf(imap, "%s EXAMINE \"%s\"\r\n", Atoms[examine], mailbox); state = Examine; } break; case Examine: { if (resp.tag == examine) { fprintf( imap, "%s UID THREAD %s UTF-8 %s\r\n", Atoms[thread], algo, search ); state = Thread; break; } if (resp.resp != AtomOk || !resp.code.len) break; enum Atom code = dataCheck(resp.code.ptr[0], Atom).atom; struct Data value = resp.code.ptr[1]; if (code == AtomUIDValidity) { uint32_t validity = dataCheck(value, Number).number; uint32_t previous = uidRead("UIDVALIDITY"); if (previous && validity != previous) { errx( EX_TEMPFAIL, "UIDVALIDITY changed; fresh export required" ); } if (!previous) uidWrite("UIDVALIDITY", validity); } else if (code == AtomUIDNext) { uidNext = dataCheck(value, Number).number; uint32_t prev = uidRead("UIDNEXT"); if (uidNext == prev) { fprintf(imap, "ayy LOGOUT\r\n"); state = Logout; } else { exitStatus = EXIT_SUCCESS; } } } break; case Thread: { if (resp.resp != AtomThread) break; if (!resp.data.len) { errx(EX_TEMPFAIL, "no messages matching %s", search); } createDirs(); threads = resp.data; resp.data = (struct List) {0}; // prevent freeing threads envelopes = calloc(threads.len, sizeof(*envelopes)); if (!envelopes) err(EX_OSERR, "calloc"); if (exportFetch(imap, export, threads)) { exportTags = 1; state = Export; } else { concatFetch(imap, concat, threads); state = Concat; } } break; case Export: { if (resp.resp == AtomFetch) { if (!resp.data.len) errx(EX_PROTOCOL, "missing FETCH data"); struct List items = dataCheck(resp.data.ptr[0], List).list; if (exportData(imap, export, items)) exportTags++; } if (resp.tag != export || --exportTags) break; concatFetch(imap, concat, threads); state = Concat; } break; case Concat: { if (resp.resp == AtomFetch) { if (!resp.data.len) errx(EX_PROTOCOL, "missing FETCH data"); // Prevent freeing data in envelopes: struct Data items = dataTake(&resp.data.ptr[0]); concatData(threads, envelopes, dataCheck(items, List).list); } if (resp.tag != concat) break; concatThreads(threads, envelopes); concatIndex(threads, envelopes); uidWrite("UIDNEXT", uidNext); fprintf(imap, "ayy LOGOUT\r\n"); state = Logout; } break; case Logout:; } } fclose(imapRead); fclose(imap); return exitStatus; }