summary refs log blame commit diff
path: root/archive.c
blob: c84b74e15e1d8311479bd1764631a71f6e918502 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17















                                                                         
                  


                     
                     

                     
                    


                                           






                                                             
 





                                                      
 



                                                                    

















































                                                                           













                                            

                                  




                                                                              






                                                                           
 



                                                                                                
 




















                                                                                                            
                                 





                                                                              
                         



                                                                                             
                                 
                                                     
                                                    





                                                                           
                                 
                         
 






                                                                                                    
                         
 







                                                                                                    
                         
                                            
                 
                               

                     
/* 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/>.
 */

#include <err.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sysexits.h>
#include <unistd.h>

#include "archive.h"
#include "imap.h"

#define ENV_PASSWORD "BUBGER_IMAP_PASSWORD"

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);
}

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 {
		Ready,
		Login,
		Examine,
		Thread,
		Export,
		Concat,
		Logout,
	} state = Ready;

	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};
	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);
		}

		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.resp == AtomOk && resp.code.len > 1) {
					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;
						}
					}
				}

				if (resp.tag != examine) break;
				fprintf(
					imap, "%s UID THREAD %s UTF-8 %s\r\n",
					Atoms[thread], algo, search
				);
				state = Thread;
			}

			break; case Thread: {
				if (resp.resp != AtomThread) break;
				if (!resp.data.len) {
					errx(EX_TEMPFAIL, "no messages matching %s", search);
				}
				createDir("UID");
				createDir("message");
				createDir("thread");
				threads = resp.data;
				resp.data = (struct List) {0};
				if (exportFetch(imap, export, threads)) {
					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");
					exportData(dataCheck(resp.data.ptr[0], List).list);
				}
				if (resp.tag != export) break;
				concatFetch(imap, concat, threads);
				state = Concat;
			}

			break; case Concat: {
				if (resp.resp == AtomFetch) {
					if (!resp.data.len) errx(EX_PROTOCOL, "missing FETCH data");
					concatData(threads, dataCheck(resp.data.ptr[0], List).list);
				}
				if (resp.tag != concat) break;
				uidWrite("UIDNEXT", uidNext);
				fprintf(imap, "ayy LOGOUT\r\n");
				state = Logout;
			}
			
			break; case Logout:;
		}
		respFree(resp);
	}
	fclose(imap);
}