/* Copyright (C) 2020 C. McEnroe <june@causal.agency>
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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 <err.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sysexits.h>
#include <unistd.h>
#include "archive.h"
#include "imap.h"
#define ENV_PASSWORD "BUBGER_IMAP_PASSWORD"
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;
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:H:S:a:h:m:p:qs:t:u: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 'a': algo = optarg;
break; case 'h': host = 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;
break; case 'y': baseStylesheet = optarg;
}
}
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 (!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;
struct IMAP imap = imapOpen(host, port);
for (
struct Resp resp;
resp = imapResp(&imap), 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.w, "%s LOGIN \"%s\" \"%s\"\r\n",
Atoms[login], user, pass
);
state = Login;
}
break; case Login: {
if (resp.tag != login) break;
fprintf(imap.w, "%s EXAMINE \"%s\"\r\n", Atoms[examine], mailbox);
state = Examine;
}
break; case Examine: {
if (resp.tag == examine) {
fprintf(
imap.w, "%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.w, "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.w, export, threads)) {
exportTags = 1;
state = Export;
} else {
concatFetch(imap.w, 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.w, export, items)) exportTags++;
}
if (resp.tag != export || --exportTags) break;
concatFetch(imap.w, 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.w, "ayy LOGOUT\r\n");
state = Logout;
}
break; case Logout:;
}
}
fclose(imap.r);
fclose(imap.w);
return exitStatus;
}