about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Makefile1
-rw-r--r--archive.c250
-rw-r--r--archive.h5
-rw-r--r--export.c271
-rw-r--r--imap.h5
5 files changed, 286 insertions, 246 deletions
diff --git a/Makefile b/Makefile
index 106ceda..000620e 100644
--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,7 @@ LDLIBS = -ltls
 
 OBJS += archive.o
 OBJS += atom.o
+OBJS += export.o
 OBJS += html.o
 OBJS += imap.o
 OBJS += mbox.o
diff --git a/archive.c b/archive.c
index b045a7a..2e8bf31 100644
--- a/archive.c
+++ b/archive.c
@@ -17,14 +17,11 @@
 #include <err.h>
 #include <errno.h>
 #include <inttypes.h>
-#include <limits.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
-#include <string.h>
 #include <sys/stat.h>
 #include <sysexits.h>
-#include <time.h>
 #include <unistd.h>
 
 #include "archive.h"
@@ -32,247 +29,6 @@
 
 #define ENV_PASSWORD "BUBGER_IMAP_PASSWORD"
 
-static const char *uidPath(uint32_t uid, const char *type) {
-	static char buf[PATH_MAX];
-	snprintf(buf, sizeof(buf), "UID/%" PRIu32 ".%s", uid, type);
-	return buf;
-}
-
-static void flatten(struct List *flat, struct List nested) {
-	for (size_t i = 0; i < nested.len; ++i) {
-		if (nested.ptr[i].type == List) {
-			flatten(flat, nested.ptr[i].list);
-		} else {
-			listPush(flat, nested.ptr[i]);
-		}
-	}
-}
-
-static enum Atom fetchNew(FILE *imap, struct List threads) {
-	struct List uids = {0};
-	flatten(&uids, threads);
-	for (size_t i = uids.len - 1; i < uids.len; --i) {
-		if (uids.ptr[i].type != Number) {
-			errx(EX_PROTOCOL, "invalid thread UID");
-		}
-		uint32_t uid = uids.ptr[i].number;
-		int error = 0;
-		if (!error) error = access(uidPath(uid, "atom"), F_OK);
-		if (!error) error = access(uidPath(uid, "html"), F_OK);
-		if (!error) error = access(uidPath(uid, "mbox"), F_OK);
-		if (!error) {
-			uids.ptr[i] = uids.ptr[--uids.len];
-		}
-	}
-	enum Atom tag = 0;
-	if (!uids.len) goto done;
-
-	int error = mkdir("UID", 0775);
-	if (error && errno != EEXIST) err(EX_CANTCREAT, "UID");
-
-	tag = atom("fetchNew");
-	fprintf(imap, "%s UID FETCH ", Atoms[tag]);
-	for (size_t i = 0; i < uids.len; ++i) {
-		fprintf(imap, "%s%" PRIu32, (i ? "," : ""), uids.ptr[i].number);
-	}
-	fprintf(
-		imap,
-		" ("
-		"UID ENVELOPE "
-		"BODY[HEADER.FIELDS (" MBOX_HEADERS ")] BODY[TEXT]"
-		")\r\n"
-	);
-
-done:
-	listFree(uids);
-	return tag;
-}
-
-static struct Address parseAddress(struct List list) {
-	if (list.len < 4) {
-		errx(EX_PROTOCOL, "missing address structure fields");
-	}
-	struct Address addr = {0};
-	if (list.ptr[0].type == String) {
-		// TODO: Decode UTF-8 in name.
-		addr.name = strdup(list.ptr[0].string);
-		if (!addr.name) err(EX_OSERR, "strdup");
-	}
-	if (list.ptr[2].type == String) addr.mailbox = list.ptr[2].string;
-	if (list.ptr[3].type == String) addr.host = list.ptr[3].string;
-	return addr;
-}
-
-static struct AddressList parseAddressList(struct List list) {
-	struct Address *addrs = calloc(list.len, sizeof(*addrs));
-	if (!addrs) err(EX_OSERR, "calloc");
-	for (size_t i = 0; i < list.len; ++i) {
-		if (list.ptr[i].type != List) {
-			errx(EX_PROTOCOL, "invalid address field");
-		}
-		addrs[i] = parseAddress(list.ptr[i].list);
-	}
-	return (struct AddressList) { list.len, addrs };
-}
-
-static char *parseID(char *id) {
-	size_t len = strlen(id);
-	if (id[0] != '<' || !len || id[len - 1] != '>') {
-		errx(EX_PROTOCOL, "invalid message ID");
-	}
-	id[len - 1] = '\0';
-	return &id[1];
-}
-
-static struct Envelope parseEnvelope(struct List list) {
-	enum {
-		Date, Subject, From, Sender, ReplyTo,
-		To, Cc, Bcc, InReplyTo, MessageID,
-		EnvelopeLen,
-	};
-	if (list.len < EnvelopeLen) {
-		errx(EX_PROTOCOL, "missing envelope structure fields");
-	}
-	struct Envelope envelope = {0};
-
-	if (list.ptr[Date].type != String) {
-		errx(EX_PROTOCOL, "invalid envelope date field");
-	}
-	const char *date = list.ptr[Date].string;
-	date = strptime(date, "%a, %e %b %Y %H:%M:%S %z", &envelope.date);
-	if (!date) errx(EX_PROTOCOL, "invalid envelope date format");
-
-	envelope.date.tm_isdst = -1;
-	envelope.utc = mktime(&envelope.date);
-
-	if (list.ptr[Subject].type != String) {
-		errx(EX_PROTOCOL, "invalid envelope subject field");
-	}
-	// TODO: Decode UTF-8 in subject.
-	envelope.subject = strdup(list.ptr[Subject].string);
-	if (!envelope.subject) err(EX_OSERR, "strdup");
-
-	for (size_t i = From; i <= Bcc; ++i) {
-		if (list.ptr[i].type == List) continue;
-		if (list.ptr[i].type == Atom && list.ptr[i].atom == AtomNil) {
-			list.ptr[i].type = List;
-			list.ptr[i].list = (struct List) {0};
-			continue;
-		}
-		errx(EX_PROTOCOL, "invalid envelope address field");
-	}
-	for (size_t i = From; i <= ReplyTo; ++i) {
-		if (!list.ptr[i].list.len || list.ptr[i].list.ptr[0].type != List) {
-			errx(EX_PROTOCOL, "invalid envelope address field");
-		}
-	}
-	envelope.from = parseAddress(list.ptr[From].list.ptr[0].list);
-	envelope.sender = parseAddress(list.ptr[Sender].list.ptr[0].list);
-	envelope.replyTo = parseAddress(list.ptr[ReplyTo].list.ptr[0].list);
-	envelope.to = parseAddressList(list.ptr[To].list);
-	envelope.cc = parseAddressList(list.ptr[Cc].list);
-	envelope.bcc = parseAddressList(list.ptr[Bcc].list);
-
-	if (list.ptr[InReplyTo].type == String) {
-		envelope.inReplyTo = parseID(list.ptr[InReplyTo].string);
-	}
-	if (list.ptr[MessageID].type != String) {
-		errx(EX_PROTOCOL, "invalid envelope message-id field");
-	}
-	envelope.messageID = parseID(list.ptr[MessageID].string);
-
-	return envelope;
-}
-
-static void exportMessage(struct List items) {
-	static enum Atom AtomUID, AtomEnvelope, AtomBody;
-	static enum Atom AtomHeaderFields, AtomText;
-	if (!AtomUID) AtomUID = atom("UID");
-	if (!AtomEnvelope) AtomEnvelope = atom("ENVELOPE");
-	if (!AtomBody) AtomBody = atom("BODY");
-	if (!AtomHeaderFields) AtomHeaderFields = atom("HEADER.FIELDS");
-	if (!AtomText) AtomText = atom("TEXT");
-
-	uint32_t uid = 0;
-	struct Envelope envelope = {0};
-	char *header = NULL;
-	char *body = NULL;
-
-	for (size_t i = 0; i + 1 < items.len; i += 2) {
-		enum Atom name;
-		if (items.ptr[i].type == Atom) {
-			name = items.ptr[i].atom;
-		} else if (
-			items.ptr[i].type == List &&
-			items.ptr[i].list.len &&
-			items.ptr[i].list.ptr[0].type == Atom
-		) {
-			name = items.ptr[i].list.ptr[0].atom;
-		} else {
-			errx(EX_PROTOCOL, "invalid data item name");
-		}
-
-		if (name == AtomBody) {
-			i--;
-			continue;
-		} else if (name == AtomUID) {
-			if (items.ptr[i + 1].type != Number) {
-				errx(EX_PROTOCOL, "invalid UID data item value");
-			}
-			uid = items.ptr[i + 1].number;
-		} else if (name == AtomEnvelope) {
-			if (items.ptr[i + 1].type != List) {
-				errx(EX_PROTOCOL, "invalid ENVELOPE data item value");
-			}
-			envelope = parseEnvelope(items.ptr[i + 1].list);
-		} else if (name == AtomHeaderFields) {
-			if (items.ptr[i + 1].type != String) {
-				errx(EX_PROTOCOL, "invalid BODY[HEADER.FIELDS] data item value");
-			}
-			header = items.ptr[i + 1].string;
-		} else if (name == AtomText) {
-			if (items.ptr[i + 1].type != String) {
-				errx(EX_PROTOCOL, "invalid BODY[TEXT] data item value");
-			}
-			body = items.ptr[i + 1].string;
-		}
-	}
-
-	if (!uid) errx(EX_PROTOCOL, "missing UID data item");
-	if (!envelope.subject) errx(EX_PROTOCOL, "missing ENVELOPE data item");
-	if (!header) errx(EX_PROTOCOL, "missing BODY[HEADER.FIELDS] data item");
-	if (!body) errx(EX_PROTOCOL, "missing BODY[TEXT] data item");
-
-	const char *path;
-	FILE *file;
-	int error;
-
-	path = uidPath(uid, "mbox");
-	file = fopen(path, "w");
-	if (!file) err(EX_CANTCREAT, "%s", path);
-	error = mboxFrom(file)
-		|| mboxHeader(file, header)
-		|| mboxBody(file, body)
-		|| fclose(file);
-	if (error) err(EX_IOERR, "%s", path);
-
-	path = uidPath(uid, "html");
-	file = fopen(path, "w");
-	if (!file) err(EX_CANTCREAT, "%s", path);
-	error = htmlEnvelope(file, &envelope)
-		|| fclose(file);
-	if (error) err(EX_IOERR, "%s", path);
-
-	path = uidPath(uid, "atom");
-	file = fopen(path, "w");
-	if (!file) err(EX_CANTCREAT, "%s", path);
-	error = atomEnvelope(file, &envelope)
-		|| fclose(file);
-	if (error) err(EX_IOERR, "%s", path);
-
-	envelopeFree(envelope);
-}
-
 static void checkValidity(uint32_t validity) {
 	FILE *file = fopen("UIDVALIDITY", "r");
 	if (file) {
@@ -393,14 +149,16 @@ int main(int argc, char *argv[]) {
 			if (!resp.data.len) {
 				errx(EX_TEMPFAIL, "no messages matching %s", search);
 			}
-			export = fetchNew(imap, resp.data);
+			int error = mkdir("UID", 0775);
+			if (error && errno != EEXIST) err(EX_CANTCREAT, "UID");
+			export = exportThreads(imap, resp.data);
 		}
 
 		if (export && resp.resp == AtomFetch) {
 			if (!resp.data.len || resp.data.ptr[0].type != List) {
 				errx(EX_PROTOCOL, "invalid FETCH data");
 			}
-			exportMessage(resp.data.ptr[0].list);
+			exportData(resp.data.ptr[0].list);
 		}
 
 		respFree(resp);
diff --git a/archive.h b/archive.h
index cc29953..540b898 100644
--- a/archive.h
+++ b/archive.h
@@ -18,6 +18,8 @@
 #include <stdlib.h>
 #include <time.h>
 
+#include "imap.h"
+
 struct Address {
 	char *name;
 	const char *mailbox;
@@ -58,6 +60,9 @@ static inline void envelopeFree(struct Envelope envelope) {
 	free(envelope.bcc.addrs);
 }
 
+enum Atom exportThreads(FILE *imap, struct List threads);
+void exportData(struct List items);
+
 #define TEMPLATE(...) #__VA_ARGS__
 
 struct Variable {
diff --git a/export.c b/export.c
new file mode 100644
index 0000000..3e87521
--- /dev/null
+++ b/export.c
@@ -0,0 +1,271 @@
+/* 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 <inttypes.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "archive.h"
+#include "imap.h"
+
+static const char *uidPath(uint32_t uid, const char *type) {
+	static char buf[PATH_MAX];
+	snprintf(buf, sizeof(buf), "UID/%" PRIu32 ".%s", uid, type);
+	return buf;
+}
+
+static void flatten(struct List *flat, struct List nested) {
+	for (size_t i = 0; i < nested.len; ++i) {
+		if (nested.ptr[i].type == List) {
+			flatten(flat, nested.ptr[i].list);
+		} else {
+			listPush(flat, nested.ptr[i]);
+		}
+	}
+}
+
+enum Atom exportThreads(FILE *imap, struct List threads) {
+	struct List uids = {0};
+	flatten(&uids, threads);
+	for (size_t i = uids.len - 1; i < uids.len; --i) {
+		if (uids.ptr[i].type != Number) {
+			errx(EX_PROTOCOL, "invalid thread UID");
+		}
+		uint32_t uid = uids.ptr[i].number;
+		int error = 0;
+		if (!error) error = access(uidPath(uid, "atom"), F_OK);
+		if (!error) error = access(uidPath(uid, "html"), F_OK);
+		if (!error) error = access(uidPath(uid, "mbox"), F_OK);
+		if (!error) {
+			uids.ptr[i] = uids.ptr[--uids.len];
+		}
+	}
+	enum Atom tag = 0;
+	if (!uids.len) goto done;
+
+	tag = atom("exportThreads");
+	fprintf(imap, "%s UID FETCH ", Atoms[tag]);
+	for (size_t i = 0; i < uids.len; ++i) {
+		fprintf(imap, "%s%" PRIu32, (i ? "," : ""), uids.ptr[i].number);
+	}
+	fprintf(
+		imap,
+		" ("
+		"UID ENVELOPE "
+		"BODY[HEADER.FIELDS (" MBOX_HEADERS ")] BODY[TEXT]"
+		")\r\n"
+	);
+
+done:
+	listFree(uids);
+	return tag;
+}
+
+static struct Address parseAddress(struct List list) {
+	if (list.len < 4) {
+		errx(EX_PROTOCOL, "missing address structure fields");
+	}
+	struct Address addr = {0};
+	if (list.ptr[0].type == String) {
+		// TODO: Decode UTF-8 in name.
+		addr.name = strdup(list.ptr[0].string);
+		if (!addr.name) err(EX_OSERR, "strdup");
+	}
+	if (list.ptr[2].type == String) addr.mailbox = list.ptr[2].string;
+	if (list.ptr[3].type == String) addr.host = list.ptr[3].string;
+	return addr;
+}
+
+static struct AddressList parseAddressList(struct List list) {
+	struct Address *addrs = calloc(list.len, sizeof(*addrs));
+	if (!addrs) err(EX_OSERR, "calloc");
+	for (size_t i = 0; i < list.len; ++i) {
+		if (list.ptr[i].type != List) {
+			errx(EX_PROTOCOL, "invalid address field");
+		}
+		addrs[i] = parseAddress(list.ptr[i].list);
+	}
+	return (struct AddressList) { list.len, addrs };
+}
+
+static char *parseID(char *id) {
+	size_t len = strlen(id);
+	if (id[0] != '<' || !len || id[len - 1] != '>') {
+		errx(EX_PROTOCOL, "invalid message ID");
+	}
+	id[len - 1] = '\0';
+	return &id[1];
+}
+
+static struct Envelope parseEnvelope(struct List list) {
+	enum {
+		Date, Subject, From, Sender, ReplyTo,
+		To, Cc, Bcc, InReplyTo, MessageID,
+		EnvelopeLen,
+	};
+	if (list.len < EnvelopeLen) {
+		errx(EX_PROTOCOL, "missing envelope structure fields");
+	}
+	struct Envelope envelope = {0};
+
+	if (list.ptr[Date].type != String) {
+		errx(EX_PROTOCOL, "invalid envelope date field");
+	}
+	const char *date = list.ptr[Date].string;
+	date = strptime(date, "%a, %e %b %Y %H:%M:%S %z", &envelope.date);
+	if (!date) errx(EX_PROTOCOL, "invalid envelope date format");
+
+	envelope.date.tm_isdst = -1;
+	envelope.utc = mktime(&envelope.date);
+
+	if (list.ptr[Subject].type != String) {
+		errx(EX_PROTOCOL, "invalid envelope subject field");
+	}
+	// TODO: Decode UTF-8 in subject.
+	envelope.subject = strdup(list.ptr[Subject].string);
+	if (!envelope.subject) err(EX_OSERR, "strdup");
+
+	for (size_t i = From; i <= Bcc; ++i) {
+		if (list.ptr[i].type == List) continue;
+		if (list.ptr[i].type == Atom && list.ptr[i].atom == AtomNil) {
+			list.ptr[i].type = List;
+			list.ptr[i].list = (struct List) {0};
+			continue;
+		}
+		errx(EX_PROTOCOL, "invalid envelope address field");
+	}
+	for (size_t i = From; i <= ReplyTo; ++i) {
+		if (!list.ptr[i].list.len || list.ptr[i].list.ptr[0].type != List) {
+			errx(EX_PROTOCOL, "invalid envelope address field");
+		}
+	}
+	envelope.from = parseAddress(list.ptr[From].list.ptr[0].list);
+	envelope.sender = parseAddress(list.ptr[Sender].list.ptr[0].list);
+	envelope.replyTo = parseAddress(list.ptr[ReplyTo].list.ptr[0].list);
+	envelope.to = parseAddressList(list.ptr[To].list);
+	envelope.cc = parseAddressList(list.ptr[Cc].list);
+	envelope.bcc = parseAddressList(list.ptr[Bcc].list);
+
+	if (list.ptr[InReplyTo].type == String) {
+		envelope.inReplyTo = parseID(list.ptr[InReplyTo].string);
+	}
+	if (list.ptr[MessageID].type != String) {
+		errx(EX_PROTOCOL, "invalid envelope message-id field");
+	}
+	envelope.messageID = parseID(list.ptr[MessageID].string);
+
+	return envelope;
+}
+
+static enum Atom AtomBody;
+static enum Atom AtomEnvelope;
+static enum Atom AtomHeaderFields;
+static enum Atom AtomText;
+static enum Atom AtomUID;
+
+void exportData(struct List items) {
+	if (!AtomBody) AtomBody = atom("BODY");
+	if (!AtomEnvelope) AtomEnvelope = atom("ENVELOPE");
+	if (!AtomHeaderFields) AtomHeaderFields = atom("HEADER.FIELDS");
+	if (!AtomText) AtomText = atom("TEXT");
+	if (!AtomUID) AtomUID = atom("UID");
+
+	uint32_t uid = 0;
+	struct Envelope envelope = {0};
+	char *header = NULL;
+	char *body = NULL;
+
+	for (size_t i = 0; i + 1 < items.len; i += 2) {
+		enum Atom name;
+		if (items.ptr[i].type == Atom) {
+			name = items.ptr[i].atom;
+		} else if (
+			items.ptr[i].type == List &&
+			items.ptr[i].list.len &&
+			items.ptr[i].list.ptr[0].type == Atom
+		) {
+			name = items.ptr[i].list.ptr[0].atom;
+		} else {
+			errx(EX_PROTOCOL, "invalid data item name");
+		}
+
+		if (name == AtomBody) {
+			i--;
+			continue;
+		} else if (name == AtomUID) {
+			if (items.ptr[i + 1].type != Number) {
+				errx(EX_PROTOCOL, "invalid UID data item value");
+			}
+			uid = items.ptr[i + 1].number;
+		} else if (name == AtomEnvelope) {
+			if (items.ptr[i + 1].type != List) {
+				errx(EX_PROTOCOL, "invalid ENVELOPE data item value");
+			}
+			envelope = parseEnvelope(items.ptr[i + 1].list);
+		} else if (name == AtomHeaderFields) {
+			if (items.ptr[i + 1].type != String) {
+				errx(EX_PROTOCOL, "invalid BODY[HEADER.FIELDS] data item value");
+			}
+			header = items.ptr[i + 1].string;
+		} else if (name == AtomText) {
+			if (items.ptr[i + 1].type != String) {
+				errx(EX_PROTOCOL, "invalid BODY[TEXT] data item value");
+			}
+			body = items.ptr[i + 1].string;
+		}
+	}
+
+	if (!uid) errx(EX_PROTOCOL, "missing UID data item");
+	if (!envelope.subject) errx(EX_PROTOCOL, "missing ENVELOPE data item");
+	if (!header) errx(EX_PROTOCOL, "missing BODY[HEADER.FIELDS] data item");
+	if (!body) errx(EX_PROTOCOL, "missing BODY[TEXT] data item");
+
+	const char *path;
+	FILE *file;
+	int error;
+
+	path = uidPath(uid, "mbox");
+	file = fopen(path, "w");
+	if (!file) err(EX_CANTCREAT, "%s", path);
+	error = mboxFrom(file)
+		|| mboxHeader(file, header)
+		|| mboxBody(file, body)
+		|| fclose(file);
+	if (error) err(EX_IOERR, "%s", path);
+
+	path = uidPath(uid, "html");
+	file = fopen(path, "w");
+	if (!file) err(EX_CANTCREAT, "%s", path);
+	error = htmlEnvelope(file, &envelope)
+		|| fclose(file);
+	if (error) err(EX_IOERR, "%s", path);
+
+	path = uidPath(uid, "atom");
+	file = fopen(path, "w");
+	if (!file) err(EX_CANTCREAT, "%s", path);
+	error = atomEnvelope(file, &envelope)
+		|| fclose(file);
+	if (error) err(EX_IOERR, "%s", path);
+
+	envelopeFree(envelope);
+}
diff --git a/imap.h b/imap.h
index e4ac7ea..f9b3106 100644
--- a/imap.h
+++ b/imap.h
@@ -14,6 +14,9 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+#ifndef IMAP_H
+#define IMAP_H
+
 #include <err.h>
 #include <stdbool.h>
 #include <stdint.h>
@@ -136,3 +139,5 @@ static inline void respFree(struct Resp resp) {
 extern bool imapVerbose;
 FILE *imapOpen(const char *host, const char *port);
 struct Resp imapResp(FILE *imap);
+
+#endif /* IMAP_H */