summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--archive.c1
-rw-r--r--archive.h15
-rw-r--r--bubger.115
-rw-r--r--export.c83
-rw-r--r--html.c27
5 files changed, 126 insertions, 15 deletions
diff --git a/archive.c b/archive.c
index 7edc6d6..64b234e 100644
--- a/archive.c
+++ b/archive.c
@@ -181,6 +181,7 @@ int main(int argc, char *argv[]) {
 					errx(EX_TEMPFAIL, "no messages matching %s", search);
 				}
 				createDir("UID");
+				createDir("attachment");
 				createDir("message");
 				createDir("thread");
 				threads = resp.data;
diff --git a/archive.h b/archive.h
index d0bc00d..06fd435 100644
--- a/archive.h
+++ b/archive.h
@@ -156,15 +156,19 @@ char *decodeHeader(const char *header);
 char *decodeToString(const struct BodyPart *part, const char *content);
 int decodeToFile(FILE *file, const struct BodyPart *part, const char *content);
 
+struct Attachment {
+	char path[3][NAME_MAX + 1];
+};
+
 static inline const char *pathUID(uint32_t uid, const char *type) {
-	static char buf[PATH_MAX];
+	static char buf[PATH_MAX + 1];
 	snprintf(buf, sizeof(buf), "UID/%" PRIu32 ".%s", uid, type);
 	return buf;
 }
 
 static inline const char *pathSafe(const char *messageID) {
 	if (!strchr(messageID, '/')) return messageID;
-	static char buf[PATH_MAX];
+	static char buf[NAME_MAX + 1];
 	strlcpy(buf, messageID, sizeof(buf));
 	for (char *ptr = buf; (ptr = strchr(ptr, '/')); ++ptr) {
 		*ptr = ';';
@@ -173,13 +177,13 @@ static inline const char *pathSafe(const char *messageID) {
 }
 
 static inline const char *pathMessage(const char *messageID, const char *type) {
-	static char buf[PATH_MAX];
+	static char buf[PATH_MAX + 1];
 	snprintf(buf, sizeof(buf), "message/%s.%s", pathSafe(messageID), type);
 	return buf;
 }
 
 static inline const char *pathThread(const char *messageID, const char *type) {
-	static char buf[PATH_MAX];
+	static char buf[PATH_MAX + 1];
 	snprintf(buf, sizeof(buf), "thread/%s.%s", pathSafe(messageID), type);
 	return buf;
 }
@@ -203,6 +207,9 @@ int atomFeedClose(FILE *file);
 extern const char *htmlTitle;
 int htmlMessageOpen(FILE *file, const struct Envelope *envelope);
 int htmlInline(FILE *file, const struct BodyPart *part, const char *content);
+int htmlAttachment(
+	FILE *file, const struct BodyPart *part, const struct Attachment *attach
+);
 int htmlMessageClose(FILE *file);
 int htmlThreadHead(FILE *file, const struct Envelope *envelope);
 int htmlThreadOpen(FILE *file, const struct Envelope *envelope);
diff --git a/bubger.1 b/bubger.1
index 57d734f..ed402a1 100644
--- a/bubger.1
+++ b/bubger.1
@@ -1,4 +1,4 @@
-.Dd April 16, 2020
+.Dd April 17, 2020
 .Dt BUBGER 1
 .Os
 .
@@ -112,6 +112,8 @@ Stores the mailbox UID validity.
 Stores the next UID of the mailbox.
 .It Pa UID/*.atom , Pa UID/*.html , Pa UID/*.mbox
 Cached Atom, HTML and mboxrd fragments for each message.
+.It Pa attachment/*/*/*
+Attached files.
 .It Pa message/*.mbox
 Rendered mboxrd files for each message.
 .It Pa thread/*.atom , Pa thread/*.html , Pa thread/*.mbox
@@ -179,6 +181,17 @@ Rendered Atom, HTML and mboxrd files for each thread.
 .Re
 .It
 .Rs
+.%A R. Troost
+.%A S. Dorner
+.%A K. Moore
+.%T The Content-Disposition Header Field
+.%I IETF
+.%N RFC 2183
+.%D August 1997
+.%U https://tools.ietf.org/html/rfc2183
+.Re
+.It
+.Rs
 .%A T. Berners-Lee
 .%A L. Masinter
 .%A M. McCahill
diff --git a/export.c b/export.c
index 21f5662..9aa3126 100644
--- a/export.c
+++ b/export.c
@@ -15,11 +15,14 @@
  */
 
 #include <err.h>
+#include <errno.h>
 #include <inttypes.h>
+#include <limits.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <strings.h>
+#include <sys/stat.h>
 #include <sysexits.h>
 #include <unistd.h>
 
@@ -115,8 +118,58 @@ static void exportAtom(
 	if (error) err(EX_IOERR, "%s", path);
 }
 
+static struct Attachment exportAttachment(
+	const struct Envelope *envelope, struct List section,
+	const struct BodyPart *part, struct Data body
+) {
+	struct Attachment attach = { "", "", "" };
+	strlcpy(
+		attach.path[0], pathSafe(envelope->messageID), sizeof(attach.path[0])
+	);
+	for (size_t i = 0; i < section.len; ++i) {
+		uint32_t num = dataCheck(section.ptr[i], Number).number;
+		char buf[32];
+		snprintf(buf, sizeof(buf), "%s%" PRIu32, (i ? "." : ""), num);
+		strlcat(attach.path[1], buf, sizeof(attach.path[1]));
+	}
+	struct List params = part->disposition.params;
+	for (size_t i = 0; i + 1 < params.len; i += 2) {
+		const char *key = dataCheck(params.ptr[i], String).string;
+		if (strcasecmp(key, "filename")) continue;
+		const char *value = dataCheck(params.ptr[i + 1], String).string;
+		strlcpy(attach.path[2], pathSafe(value), sizeof(attach.path[2]));
+	}
+	if (!attach.path[2][0]) {
+		const char *disposition = part->disposition.type;
+		if (!disposition) disposition = "INLINE";
+		strlcat(attach.path[2], pathSafe(disposition), sizeof(attach.path[2]));
+		strlcat(attach.path[2], ".", sizeof(attach.path[2]));
+		strlcat(attach.path[2], pathSafe(part->subtype), sizeof(attach.path[2]));
+	}
+
+	char path[PATH_MAX + 1] = "attachment";
+	for (int i = 0; i < 2; ++i) {
+		strlcat(path, "/", sizeof(path));
+		strlcat(path, attach.path[i], sizeof(path));
+		int error = mkdir(path, 0775);
+		if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", path);
+	}
+	strlcat(path, "/", sizeof(path));
+	strlcat(path, attach.path[2], sizeof(path));
+
+	FILE *file = fopen(path, "w");
+	if (!file) err(EX_CANTCREAT, "%s", path);
+
+	int error = 0
+		|| decodeToFile(file, part, dataCheck(body, String).string)
+		|| fclose(file);
+	if (error) err(EX_IOERR, "%s", path);
+
+	return attach;
+}
+
 static int exportHTMLBody(
-	FILE *file, struct List *section,
+	FILE *file, const struct Envelope *envelope, struct List *section,
 	const struct BodyPart *part, struct Data body
 ) {
 	int error = 0;
@@ -124,36 +177,46 @@ static int exportHTMLBody(
 		for (size_t i = part->parts.len - 1; i < part->parts.len; --i) {
 			if (!isInline(&part->parts.ptr[i])) continue;
 			return exportHTMLBody(
-				file, section, &part->parts.ptr[i],
-				dataCheck(body, List).list.ptr[i]
+				file, envelope, section,
+				&part->parts.ptr[i], dataCheck(body, List).list.ptr[i]
 			);
 		}
 		return exportHTMLBody(
-			file, section, &part->parts.ptr[part->parts.len - 1],
+			file, envelope, section,
+			&part->parts.ptr[part->parts.len - 1],
 			dataCheck(body, List).list.ptr[part->parts.len - 1]
 		);
+
 	} else if (part->multipart) {
 		for (size_t i = 0; i < part->parts.len; ++i) {
 			struct Data num = { .type = Number, .number = 1 + i };
 			listPush(section, num);
 			error = exportHTMLBody(
-				file, section, &part->parts.ptr[i],
-				dataCheck(body, List).list.ptr[i]
+				file, envelope, section,
+				&part->parts.ptr[i], dataCheck(body, List).list.ptr[i]
 			);
 			if (error) return error;
 			section->len--;
 		}
-	} else if (part->message.envelope) {
+
+	} else if (part->message.structure) {
+		const struct BodyPart *structure = part->message.structure;
 		error = 0
 			|| htmlMessageOpen(file, part->message.envelope)
-			|| exportHTMLBody(file, section, part->message.structure, body)
+			|| exportHTMLBody(file, envelope, section, structure, body)
 			|| htmlMessageClose(file);
+
 	} else if (isInline(part)) {
 		char *content = decodeToString(part, dataCheck(body, String).string);
 		error = htmlInline(file, part, content);
 		free(content);
+
 	} else {
-		// TODO: Write out attachment.
+		// TODO: Open and close attachment lists.
+		struct Attachment attach = exportAttachment(
+			envelope, *section, part, body
+		);
+		error = htmlAttachment(file, part, &attach);
 	}
 	return error;
 }
@@ -170,7 +233,7 @@ static void exportHTML(
 	if (error) err(EX_IOERR, "%s", path);
 
 	struct List section = {0};
-	error = exportHTMLBody(file, &section, structure, body);
+	error = exportHTMLBody(file, envelope, &section, structure, body);
 	if (error) err(EX_IOERR, "%s", path);
 	listFree(section);
 
diff --git a/html.c b/html.c
index 9e37db8..2d24ce2 100644
--- a/html.c
+++ b/html.c
@@ -164,6 +164,33 @@ int htmlInline(FILE *file, const struct BodyPart *part, const char *content) {
 	return templateRender(file, template, vars, escapeXML);
 }
 
+static char *htmlAttachmentURL(const struct Attachment *attach) {
+	struct Variable vars[] = {
+		{ "path0", attach->path[0] },
+		{ "path1", attach->path[1] },
+		{ "path2", attach->path[2] },
+		{0},
+	};
+	return templateURL("../attachment/[path0]/[path1]/[path2]", vars);
+}
+
+int htmlAttachment(
+	FILE *file, const struct BodyPart *part, const struct Attachment *attach
+) {
+	const char *template = TEMPLATE(
+		<a class="attachment" href="[url]">[name]</a>
+	);
+	char *url = htmlAttachmentURL(attach);
+	struct Variable vars[] = {
+		{ "url", url },
+		{ "name", attach->path[2] }, // FIXME: Show intended name or type.
+		{0},
+	};
+	int error = templateRender(file, template, vars, escapeXML);
+	free(url);
+	return error;
+}
+
 int htmlMessageClose(FILE *file) {
 	return templateRender(file, TEMPLATE(</article>), NULL, NULL);
 }