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












                                                                         


                                                                 
                                                                     


                                                                       
                                                                   
                

                
                  


                     
                   
                     

                     
                    


                                           
                         
                       
                          
                           
 






                                                             
 





                                                      
 



                                                                    





                                
                                  
                           







                                        
                                                                                      



                                                                         
                                                             
                                                         
                                                       
                                                       
                                                             
                                                       
                                                                   
                                                                
                                                            
                                                            
                                                                 
                 

                                                    
                                                   



                                                           
                                            
                                           














                                                                       







                        
                              




                                            

                                  
                                          
 
                                                
                                 
                                                             
                              
                                                                  
                                                                              
                 

                                            
                                                                             


                                                                
 
                                                             
                                                                                                  
                                                
 
                                              
                                                          
                                                                                        


                                                                           
                                 
 

















                                                                                                    
                                                                                  



                                                                          
                         



                                                                                             
                                 
                                             
 
                                                    
                                                                                         

                                                                                    
                                                                           
                                                       
                                                       
                                                                             
                                                       
                                 
                         
 

                                                                                                    
                                                                                                   
                                                                                            
                                 
                                                                              
                                                                     
                                               
                         
 

                                                                                                    
                                                                             
                                                                                                    
                                                              
                                                                  
                                                             
                                                                  
                                               
                         
                                            
                 
         
                       
                          
 
/* 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;
}