summary refs log tree commit diff
diff options
context:
space:
mode:
authorJune McEnroe <june@causal.agency>2020-01-26 14:20:03 -0500
committerJune McEnroe <june@causal.agency>2020-01-26 14:20:03 -0500
commitb39c1b782e06b59f46182ac256273390fa7f87ec (patch)
tree10f185c1dac0bbfc3c7f9c31e66712527fd1977e
parentAdd -w option to manual page (diff)
downloadnotemap-b39c1b782e06b59f46182ac256273390fa7f87ec.tar.gz
notemap-b39c1b782e06b59f46182ac256273390fa7f87ec.zip
Implement IMAP flow outline
-rw-r--r--notemap.c321
1 files changed, 320 insertions, 1 deletions
diff --git a/notemap.c b/notemap.c
index fbb6e8d..d05f9c0 100644
--- a/notemap.c
+++ b/notemap.c
@@ -14,15 +14,130 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+#include <ctype.h>
 #include <err.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
 #include <sysexits.h>
+#include <time.h>
+#include <tls.h>
 #include <unistd.h>
 
+#ifndef NO_READPASSPHRASE_H
+#include <readpassphrase.h>
+#endif
+
+#if !defined(DIG_PATH) && !defined(DRILL_PATH)
+#	ifdef __FreeBSD__
+#		define DRILL_PATH "/usr/bin/drill"
+#	else
+#		define DIG_PATH "dig"
+#	endif
+#endif
+
 typedef unsigned char byte;
 
+static bool verbose;
+
+int tlsRead(void *_tls, char *ptr, int len) {
+	struct tls *tls = _tls;
+	ssize_t ret;
+	do {
+		ret = tls_read(tls, ptr, len);
+	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+	if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(tls));
+	if (verbose) fprintf(stderr, "%.*s", (int)ret, ptr);
+	return ret;
+}
+
+int tlsWrite(void *_tls, const char *ptr, int len) {
+	struct tls *tls = _tls;
+	ssize_t ret;
+	do {
+		ret = tls_write(tls, ptr, len);
+	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+	if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(tls));
+	if (verbose) fprintf(stderr, "%.*s", (int)ret, ptr);
+	return ret;
+}
+
+int tlsClose(void *_tls) {
+	struct tls *tls = _tls;
+	int error = tls_close(tls);
+	if (error) errx(EX_IOERR, "tls_close: %s", tls_error(tls));
+	return error;
+}
+
+static void lookup(const char **host, const char **port, const char *domain) {
+	static char buf[1024];
+	snprintf(buf, sizeof(buf), "_imaps._tcp.%s", domain);
+
+	int rw[2];
+	int error = pipe(rw);
+	if (error) err(EX_OSERR, "pipe");
+
+	pid_t pid = fork();
+	if (pid < 0) err(EX_OSERR, "fork");
+
+	if (!pid) {
+		close(rw[0]);
+		dup2(rw[1], STDOUT_FILENO);
+		dup2(rw[1], STDERR_FILENO);
+		close(rw[1]);
+#ifdef DRILL_PATH
+		execlp(DRILL_PATH, DRILL_PATH, buf, "SRV", NULL);
+		err(EX_CONFIG, "%s", DRILL_PATH);
+#else
+		execlp(DIG_PATH, DIG_PATH, "-t", "SRV", "-q", buf, "+short", NULL);
+		err(EX_CONFIG, "%s", DIG_PATH);
+#endif
+	}
+
+	int status;
+	pid = wait(&status);
+	if (pid < 0) err(EX_OSERR, "wait");
+
+	close(rw[1]);
+	FILE *pipe = fdopen(rw[0], "r");
+	if (!pipe) err(EX_IOERR, "fdopen");
+
+	fgets(buf, sizeof(buf), pipe);
+	if (ferror(pipe)) err(EX_IOERR, "fgets");
+
+	if (!WIFEXITED(status) || WEXITSTATUS(status)) {
+		fprintf(stderr, "%s", buf);
+		exit(WEXITSTATUS(status));
+	}
+
+	char *ptr = buf;
+#ifdef DRILL_PATH
+	for (;;) {
+		char *line = fgets(buf, sizeof(buf), pipe);
+		if (!line || !strcmp(line, ";; ANSWER SECTION:\n")) break;
+	}
+	fgets(buf, sizeof(buf), pipe);
+	if (ferror(pipe)) err(EX_IOERR, "fgets");
+	ptr = strrchr(buf, '\t');
+	ptr = (ptr ? ptr + 1 : buf);
+#endif
+	fclose(pipe);
+
+	char *dot = strrchr(ptr, '.');
+	if (dot) *dot = '\0';
+	strsep(&ptr, " \n"); // priority
+	strsep(&ptr, " \n"); // weight
+	*port = strsep(&ptr, " \n");
+	*host = strsep(&ptr, " \n");
+	if (!*host) {
+		*host = domain;
+		*port = "imaps";
+	}
+}
+
 static const char *uuidGen(void) {
 	byte uuid[16];
 	arc4random_buf(uuid, sizeof(uuid));
@@ -43,18 +158,90 @@ static const char *uuidGen(void) {
 	return str;
 }
 
+static bool uuidCheck(const char *uuid) {
+	if (strlen(uuid) != 36) return false;
+	if (strspn(uuid, "0123456789abcdef-") != 36) return false;
+	return true;
+}
+
+#define ENUM_ATOM \
+	X(Unknown, "") \
+	X(Untagged, "*") \
+	X(Ok, "OK") \
+	X(No, "NO") \
+	X(Bad, "BAD") \
+	X(Bye, "BYE") \
+	X(Login, "LOGIN") \
+	X(Search, "SEARCH") \
+	X(Fetch, "FETCH") \
+	X(Append, "APPEND") \
+	X(Next, "next")
+
+enum Atom {
+#define X(id, _) id,
+	ENUM_ATOM
+#undef X
+	AtomsLen,
+};
+
+static const char *Atoms[AtomsLen] = {
+#define X(id, str) [id] = str,
+	ENUM_ATOM
+#undef X
+};
+
+static enum Atom atom(const char *str) {
+	if (!str) return Unknown;
+	for (enum Atom i = 0; i < AtomsLen; ++i) {
+		if (!strcmp(str, Atoms[i])) return i;
+	}
+	return Unknown;
+}
+
+static void
+append(FILE *imap, const char *from, const char *uuid, const char *path) {
+}
+
 int main(int argc, char *argv[]) {
 	const char *path = ".notemap";
 	bool add = false;
+	bool force = false;
+
+	const char *user = NULL;
+	const char *host = NULL;
+	const char *port = "imaps";
+	const char *mailbox = "Notes";
+	int rppFlags = 0;
 
 	int opt;
-	while (0 < (opt = getopt(argc, argv, "am:"))) {
+	while (0 < (opt = getopt(argc, argv, "M:afh:m:p:u:vw"))) {
 		switch (opt) {
+			break; case 'M': mailbox = optarg;
 			break; case 'a': add = true;
+			break; case 'f': force = true;
+			break; case 'h': host = optarg;
 			break; case 'm': path = optarg;
+			break; case 'p': port = optarg;
+			break; case 'u': user = optarg;
+			break; case 'v': verbose = true;
+			break; case 'w': rppFlags |= RPP_STDIN;
 			break; default:  return EX_USAGE;
 		}
 	}
+	if (!user) errx(EX_USAGE, "username required");
+	
+	if (!host) {
+		const char *domain = strchr(user, '@');
+		if (!domain) errx(EX_USAGE, "no domain in username");
+		lookup(&host, &port, &domain[1]);
+	}
+
+	char buf[1024];
+	char *pass = readpassphrase(
+		(rppFlags & RPP_STDIN ? "" : "Password: "),
+		buf, sizeof(buf), rppFlags
+	);
+	if (!pass) err(EX_UNAVAILABLE, "readpassphrase");
 
 	if (add) {
 		FILE *map = fopen(path, "a");
@@ -67,4 +254,136 @@ int main(int argc, char *argv[]) {
 		int error = fclose(map);
 		if (error) err(EX_IOERR, "%s", path);
 	}
+
+	FILE *map = fopen(path, "r");
+	if (!map) err(EX_NOINPUT, "%s", path);
+
+	struct tls *client = tls_client();
+	if (!client) errx(EX_SOFTWARE, "tls_client");
+
+	struct tls_config *config = tls_config_new();
+	if (!config) errx(EX_SOFTWARE, "tls_config_new");
+
+	int error = tls_configure(client, config);
+	if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client));
+	tls_config_free(config);
+
+	error = tls_connect(client, host, port);
+	if (error) errx(EX_NOHOST, "tls_connect: %s", tls_error(client));
+
+	FILE *imap = funopen(client, tlsRead, tlsWrite, NULL, tlsClose);
+	if (!imap) err(EX_SOFTWARE, "funopen");
+	setlinebuf(imap);
+
+	char *line = NULL;
+	size_t lineCap = 0;
+
+	char *entry = NULL;
+	size_t entryCap = 0;
+	char *uuid = NULL;
+	char *note = NULL;
+	int seq = 0;
+
+	bool login = false;
+	while (0 < getline(&line, &lineCap, imap)) {
+		char *cr = strchr(line, '\r');
+		if (cr) *cr = '\0';
+
+		char *rest = line;
+		enum Atom tag = atom(strsep(&rest, " "));
+		if (rest && isdigit(rest[0])) {
+			strsep(&rest, " ");
+		}
+		enum Atom resp = atom(strsep(&rest, " "));
+
+		if (resp == No || resp == Bad || resp == Bye) {
+			errx(
+				EX_CONFIG, "%s: %s %s",
+				Atoms[tag], Atoms[resp], (rest ? rest : "")
+			);
+		}
+
+		switch (tag) {
+			break; case Untagged: {
+				if (login) break;
+				fprintf(
+					imap, "%s LOGIN \"%s\" \"%s\"\r\n",
+					Atoms[Login], user, pass
+				);
+				login = true;
+			}
+
+			break; case Login: {
+				fprintf(imap, "%s SELECT \"%s\"\r\n", Atoms[Next], mailbox);
+			}
+
+			break; case Next: {
+				ssize_t len;
+next:
+				len = getline(&entry, &entryCap, map);
+				if (len < 0) err(EX_IOERR, "%s", path);
+				if (entry[len - 1] == '\n') entry[len - 1] = '\0';
+
+				note = entry;
+				uuid = strsep(&note, " ");
+				if (!note || !uuid || !uuidCheck(uuid)) {
+					errx(EX_CONFIG, "invalid map entry: %s", entry);
+				}
+
+				// TODO: Skip note if not in argv.
+				fprintf(
+					imap,
+					"%s SEARCH HEADER X-Universally-Unique-Identifier %s\r\n",
+					Atoms[Search], uuid
+				);
+			}
+
+			break; case Search: {
+				if (!seq) goto append;
+				fprintf(imap, "%s FETCH %d INTERNALDATE\r\n", Atoms[Fetch], seq);
+			}
+
+			break; case Fetch: {
+append:
+				append(imap, user, uuid, note);
+			}
+
+			break; case Append: {
+				if (!seq) goto next;
+				fprintf(
+					imap, "%s STORE %d +FLAGS.SILENT (\\Deleted)\r\n",
+					Atoms[Next], seq
+				);
+			}
+
+			break; default:;
+		}
+
+		if (resp == Search) {
+			if (rest) {
+				seq = strtol(rest, &rest, 10);
+				if (*rest) {
+					errx(EX_CONFIG, "multiple messages matching %s", uuid);
+				}
+			} else {
+				seq = 0;
+			}
+		}
+
+		if (resp == Fetch) {
+			struct tm date = {0};
+			rest = strptime(
+				rest, "(INTERNALDATE \"%d-%b-%Y %H:%M:%S %z\")", &date
+			);
+			if (!rest) errx(EX_PROTOCOL, "invalid INTERNALDATE");
+
+			struct stat status;
+			int error = stat(note, &status);
+			if (error) err(EX_NOINPUT, "%s", note);
+
+			if (!force && status.st_mtime < mktime(&date)) {
+				errx(EX_TEMPFAIL, "%s: note has been modified in mailbox", note);
+			}
+		}
+	}
 }