about summary refs log tree commit diff
path: root/imap.c
diff options
context:
space:
mode:
Diffstat (limited to 'imap.c')
-rw-r--r--imap.c222
1 files changed, 222 insertions, 0 deletions
diff --git a/imap.c b/imap.c
new file mode 100644
index 0000000..6894319
--- /dev/null
+++ b/imap.c
@@ -0,0 +1,222 @@
+/* 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 "compat.h"
+
+#include <err.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <tls.h>
+
+#include "imap.h"
+
+const char *Atoms[AtomCap] = {
+#define X(id, str) [id] = str,
+	ENUM_ATOM
+#undef X
+};
+
+bool imapVerbose;
+
+static int imapRead(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 (imapVerbose) fprintf(stderr, "%.*s", (int)ret, ptr);
+	return ret;
+}
+
+static int imapWrite(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 (imapVerbose) fprintf(stderr, "%.*s", (int)ret, ptr);
+	return ret;
+}
+
+static int imapClose(void *_tls) {
+	struct tls *tls = _tls;
+	int error = tls_close(tls);
+	if (error) errx(EX_IOERR, "tls_close: %s", tls_error(tls));
+	return error;
+}
+
+FILE *imapOpen(const char *host, const char *port) {
+	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, imapRead, imapWrite, NULL, imapClose);
+	if (!imap) err(EX_SOFTWARE, "funopen");
+
+	setlinebuf(imap);
+	return imap;
+}
+
+static size_t cap;
+static char *buf;
+static char *ptr;
+
+static void imapLine(FILE *imap) {
+	ssize_t len = getline(&buf, &cap, imap);
+	if (len < 0) errx(EX_PROTOCOL, "unexpected eof");
+	if (len < 1 || buf[len - 1] != '\n') errx(EX_PROTOCOL, "missing LF");
+	if (len < 2 || buf[len - 2] != '\r') errx(EX_PROTOCOL, "missing CR");
+	buf[len - 2] = '\0';
+	ptr = buf;
+}
+
+static struct Data parseAtom(void) {
+	size_t len = strcspn(ptr, " ()[]{\"");
+	struct Data data = {
+		.type = Atom,
+		.atom = atomn(ptr, len),
+	};
+	ptr += len;
+	return data;
+}
+
+static struct Data parseNumber(void) {
+	return (struct Data) {
+		.type = Number,
+		.number = strtoull(ptr, &ptr, 10),
+	};
+}
+
+static struct Data parseQuoted(void) {
+	ptr++;
+	size_t len = strcspn(ptr, "\"");
+	if (ptr[len] != '"') errx(EX_PROTOCOL, "missing quoted string delimiter");
+	struct Data data = {
+		.type = String,
+		.string = strndup(ptr, len),
+	};
+	if (!data.string) err(EX_OSERR, "strndup");
+	ptr += len + 1;
+	return data;
+}
+
+static struct Data parseLiteral(FILE *imap) {
+	ptr++;
+	size_t len = strtoull(ptr, &ptr, 10);
+	if (*ptr != '}') errx(EX_PROTOCOL, "missing literal prefix delimiter");
+	struct Data data = {
+		.type = String,
+		.string = malloc(len + 1),
+	};
+	if (!data.string) err(EX_OSERR, "malloc");
+	size_t n = fread(data.string, len, 1, imap);
+	if (!n) errx(EX_PROTOCOL, "truncated literal");
+	imapLine(imap);
+	data.string[len] = '\0';
+	return data;
+}
+
+static struct Data parseData(FILE *imap);
+
+static struct Data parseList(FILE *imap, char close) {
+	if (*ptr) ptr++;
+	struct Data data = { .type = List };
+	while (*ptr != close) {
+		if (data.list.len == data.list.cap) {
+			if (data.list.cap) {
+				data.list.cap *= 2;
+			} else {
+				data.list.cap = 4;
+			}
+			data.list.ptr = realloc(
+				data.list.ptr, sizeof(*data.list.ptr) * data.list.cap
+			);
+			if (!data.list.ptr) err(EX_OSERR, "realloc");
+		}
+		data.list.ptr[data.list.len++] = parseData(imap);
+	}
+	if (*ptr) ptr++;
+	return data;
+}
+
+static struct Data parseData(FILE *imap) {
+	if (*ptr == ' ') ptr++;
+	if (*ptr == '"') return parseQuoted();
+	if (*ptr == '{') return parseLiteral(imap);
+	if (*ptr == '(') return parseList(imap, ')');
+	if (*ptr == '[') return parseList(imap, ']');
+	if (*ptr >= '0' && *ptr <= '9') return parseNumber();
+	if (*ptr) return parseAtom();
+	errx(EX_PROTOCOL, "unexpected eof");
+}
+
+struct Resp imapResp(FILE *imap) {
+	struct Data data;
+	struct Resp resp = {0};
+	imapLine(imap);
+
+	data = parseData(imap);
+	if (data.type != Atom) errx(EX_PROTOCOL, "expected tag atom");
+	resp.tag = data.atom;
+	if (resp.tag == AtomContinue) {
+		if (*ptr == ' ') ptr++;
+		resp.text = ptr;
+		return resp;
+	}
+
+	data = parseData(imap);
+	if (data.type == Number) {
+		resp.number = data.number;
+		data = parseData(imap);
+	}
+	if (data.type != Atom) errx(EX_PROTOCOL, "expected response atom");
+	resp.resp = data.atom;
+
+	if (
+		resp.resp == AtomOk ||
+		resp.resp == AtomNo ||
+		resp.resp == AtomBad ||
+		resp.resp == AtomPreauth ||
+		resp.resp == AtomBye
+	) {
+		if (*ptr == ' ') ptr++;
+		if (*ptr == '[') {
+			data = parseList(imap, ']');
+			resp.code = data.list;
+		}
+		if (*ptr == ' ') ptr++;
+		resp.text = ptr;
+	} else {
+		data = parseList(imap, '\0');
+		resp.data = data.list;
+	}
+
+	return resp;
+}