commit 6a7276de9b85b30b80dfdc1fce5f0d9013f6c19f
parent 6468fcecce6dbbda3d5e6c07b216648389ee743d
Author: Nathaniel Chappelle <nathaniel@chappelle.dev>
Date: Sat, 24 Jan 2026 15:52:23 -0800
Damn html generation is way simpler than expected
Diffstat:
| M | Makefile | | | 2 | +- |
| M | stamail.c | | | 287 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| A | style.css | | | 14 | ++++++++++++++ |
3 files changed, 300 insertions(+), 3 deletions(-)
diff --git a/Makefile b/Makefile
@@ -15,7 +15,7 @@ json.o: json.c json.h jsmn.h
$(CC) $(CFLAGS) -c json.c
clean:
- rm -f $(OBJ) $(BIN)
+ rm -rf $(OBJ) $(BIN) ./output/
messages:
notmuch show --format=json '*' | ./stamail
diff --git a/stamail.c b/stamail.c
@@ -1,5 +1,10 @@
#include <stdio.h>
#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <errno.h>
#include "json.h"
#include "stamail.h"
@@ -21,6 +26,7 @@ static void print_message(struct message *m, void *ud) {
static struct message_node *head = NULL;
static struct message_node *tail = NULL;
+static size_t message_count = 0;
/* Current callback handler */
static void collect_message(struct message *m, void *ud) {
@@ -42,10 +48,247 @@ static void collect_message(struct message *m, void *ud) {
tail->next = n;
tail = n;
}
+ message_count++;
}
+/* HTML Escape Function */
+static void fprinthtml(FILE *fp, const char *s, int len) {
+ if (!s || len <= 0)
+ return;
+
+ for (int i = 0; i < len; i++) {
+ switch (s[i]) {
+ case '<': fputs("<", fp); break;
+ case '>': fputs(">", fp); break;
+ case '&': fputs("&", fp); break;
+ case '"': fputs(""", fp); break;
+ case '\'': fputs("'", fp); break;
+ default: fputc(s[i], fp); break;
+ }
+ }
+}
+
+/* Simple hash of message-id for filename */
+static unsigned long hash_msgid(const char *s, int len) {
+ unsigned long hash = 5381;
+ for (int i = 0; i < len; i++)
+ hash = ((hash << 5) + hash) + (unsigned char)s[i];
+ return hash;
+}
+
+/* Write HTML header */
+static void writeheader(FILE *fp, const char *title) {
+ fprintf(fp, "<!DOCTYPE html>\n");
+ fprintf(fp, "<html>\n");
+ fprintf(fp, "<head>\n");
+ fprintf(fp, "<meta charset=\"UTF-8\">\n");
+ fprintf(fp, "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n");
+ fprintf(fp, "<title>%s</title>\n", title);
+ fprintf(fp, "<link rel=\"stylesheet\" href=\"style.css\">\n");
+ fprintf(fp, "</head>\n");
+ fprintf(fp, "<body>\n");
+}
+
+/* Write HTML footer */
+static void writefooter(FILE *fp) {
+ fprintf(fp, "</body>\n");
+ fprintf(fp, "</html>\n");
+}
+
+/* Create output directory if it doesn't exist */
+static int ensure_dir(const char *path) {
+ struct stat st;
+ if (stat(path, &st) == 0)
+ return 0; /* Already exists */
+
+ if (mkdir(path, 0755) != 0 && errno != EEXIST) {
+ perror(path);
+ return -1;
+ }
+ return 0;
+}
+
+/* Copy style.css from source to output directory */
+static int copy_stylesheet(const char *source, const char *output_dir) {
+ FILE *src, *dst;
+ char dst_path[1024];
+ char buf[4096];
+ size_t n;
+
+ /* Open source file */
+ src = fopen(source, "r");
+ if (!src) {
+ perror(source);
+ return -1;
+ }
+
+ /* Open destination file */
+ snprintf(dst_path, sizeof(dst_path), "%s/style.css", output_dir);
+ dst = fopen(dst_path, "w");
+ if (!dst) {
+ perror(dst_path);
+ fclose(src);
+ return -1;
+ }
+
+ /* Copy contents */
+ while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
+ if (fwrite(buf, 1, n, dst) != n) {
+ perror("fwrite");
+ fclose(src);
+ fclose(dst);
+ return -1;
+ }
+ }
+
+ fclose(src);
+ fclose(dst);
+ return 0;
+}
+
+/* Write index.html */
+static int write_index(const char *output_dir, const char *archive_name) {
+ FILE *fp;
+ char path[1024];
+
+ snprintf(path, sizeof(path), "%s/index.html", output_dir);
+
+ fp = fopen(path, "w");
+ if (!fp) {
+ perror(path);
+ return -1;
+ }
+
+ writeheader(fp, archive_name);
+
+ fprintf(fp, "<h1>%s</h1>\n", archive_name);
+ fprintf(fp, "<nav>\n");
+ fprintf(fp, "<a href=\"index.html\">Messages</a> | ");
+ fprintf(fp, "<a href=\"threads.html\">Threads</a>\n");
+ fprintf(fp, "</nav>\n");
+
+ fprintf(fp, "<p>%zu messages</p>\n", message_count);
+
+ fprintf(fp, "<table>\n");
+ fprintf(fp, "<thead>\n");
+ fprintf(fp, "<tr><th>Date</th><th>From</th><th>Subject</th></tr>\n");
+ fprintf(fp, "</thead>\n");
+ fprintf(fp, "<tbody>\n");
+
+ for (struct message_node *n = head; n; n = n->next) {
+ struct message *m = &n->m;
+ unsigned long hash = hash_msgid(m->id, m->id_len);
+
+ fprintf(fp, "<tr>");
+
+ /* Date */
+ fprintf(fp, "<td class=\"date\">");
+ fprinthtml(fp, m->date, m->date_len);
+ fprintf(fp, "</td>");
+
+ /* From */
+ fprintf(fp, "<td class=\"from\">");
+ fprinthtml(fp, m->from, m->from_len);
+ fprintf(fp, "</td>");
+
+ /* Subject (with link to message page) */
+ fprintf(fp, "<td class=\"subject\">");
+ fprintf(fp, "<a href=\"msg-%08lx.html\">", hash);
+ fprinthtml(fp, m->subject, m->subject_len);
+ fprintf(fp, "</a>");
+ fprintf(fp, "</td>");
+
+ fprintf(fp, "</tr>\n");
+ }
+
+ fprintf(fp, "</tbody>\n");
+ fprintf(fp, "</table>\n");
+
+ writefooter(fp);
+ fclose(fp);
+ return 0;
+}
+
+/* Write individual message page */
+static int write_message(const char *output_dir, struct message *m) {
+ FILE *fp;
+ char path[1024];
+ unsigned long hash = hash_msgid(m->id, m->id_len);
+
+ snprintf(path, sizeof(path), "%s/msg-%08lx.html", output_dir, hash);
+
+ fp = fopen(path, "w");
+ if (!fp) {
+ perror(path);
+ return -1;
+ }
+
+ /* Title from subject */
+ writeheader(fp, "Message");
+
+ fprintf(fp, "<nav>\n");
+ fprintf(fp, "<a href=\"index.html\">← Back to index</a>\n");
+ fprintf(fp, "</nav>\n");
+
+ fprintf(fp, "<div style=\"margin: 20px 0; padding: 15px; background: #f5f5f5; border-left: 3px solid #666;\">\n");
+
+ /* From */
+ fprintf(fp, "<div><strong>From:</strong> ");
+ fprinthtml(fp, m->from, m->from_len);
+ fprintf(fp, "</div>\n");
+
+ /* Date */
+ fprintf(fp, "<div><strong>Date:</strong> ");
+ fprinthtml(fp, m->date, m->date_len);
+ fprintf(fp, "</div>\n");
+
+ /* Subject */
+ fprintf(fp, "<div><strong>Subject:</strong> ");
+ fprinthtml(fp, m->subject, m->subject_len);
+ fprintf(fp, "</div>\n");
+
+ /* Message-ID */
+ fprintf(fp, "<div style=\"font-size: 0.9em; color: #666;\"><strong>Message-ID:</strong> ");
+ fprinthtml(fp, m->id, m->id_len);
+ fprintf(fp, "</div>\n");
+
+ fprintf(fp, "</div>\n");
+
+ /* Body */
+ if (m->body && m->body_len > 0) {
+ fprintf(fp, "<pre style=\"white-space: pre-wrap; font-family: monospace; padding: 15px; background: #fafafa; border: 1px solid #ddd;\">");
+ fprinthtml(fp, m->body, m->body_len);
+ fprintf(fp, "</pre>\n");
+ }
+
+ writefooter(fp);
+ fclose(fp);
+ return 0;
+}
+
+int main(int argc, char *argv[]) {
+ /* CONFIG */
+ const char *output_dir = "./output";
+ const char *archive_name = "Mail Archive";
+ const char *stylesheet = "./style.css";
+
+ /* Simple argument parsing */
+ for (int i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
+ output_dir = argv[++i];
+ } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
+ archive_name = argv[++i];
+ } else if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) {
+ stylesheet = argv[++i];
+ } else if (strcmp(argv[i], "-h") == 0) {
+ printf("usage: stamail [-o output_dir] [-n archive_name] [-s style.css]\n");
+ printf(" -o output directory (default: .)\n");
+ printf(" -n archive name\n");
+ printf(" -s path to style.css to copy\n");
+ return 0;
+ }
+ }
-int main(void) {
/* Slurp stdin */
char *buf = NULL;
size_t cap = 0, len = 0;
@@ -63,7 +306,10 @@ int main(void) {
buf[len++] = c;
}
- if (!buf) return 0;
+ if (!buf) {
+ fprintf(stderr, "No input\n");
+ return 1;
+ }
parse_mail_json(buf, len, collect_message, NULL);
@@ -80,6 +326,43 @@ int main(void) {
printf("--------------------------------------------------\n");
}
+ if (message_count == 0) {
+ fprintf(stderr, "No messages found\n");
+ free(buf);
+ return 1;
+ }
+
+ /* Create output directory */
+ if (ensure_dir(output_dir) != 0) {
+ free(buf);
+ return 1;
+ }
+
+ /* Generate HTML files */
+ fprintf(stderr, "Generating HTML for %zu messages...\n", message_count);
+
+ /* Copy stylesheet if provided */
+ if (stylesheet) {
+ if (copy_stylesheet(stylesheet, output_dir) != 0) {
+ fprintf(stderr, "Warning: failed to copy stylesheet\n");
+ }
+ }
+
+ if (write_index(output_dir, archive_name) != 0) {
+ free(buf);
+ return 1;
+ }
+
+ /* Write individual message pages */
+ for (struct message_node *n = head; n; n = n->next) {
+ if (write_message(output_dir, &n->m) != 0) {
+ free(buf);
+ return 1;
+ }
+ }
+
+ fprintf(stderr, "Done! Output written to %s/\n", output_dir);
+
free(buf);
struct message_node *n = head;
while (n) {
diff --git a/style.css b/style.css
@@ -0,0 +1,14 @@
+* { margin: 0; padding: 0; box-sizing: border-box; }
+body { font-family: monospace; padding: 20px; max-width: 1200px; margin: 0 auto; background: #fff; color: #000; }
+h1 { margin-bottom: 10px; font-size: 1.5em; }
+nav { margin: 20px 0; padding: 10px 0; border-bottom: 1px solid #ccc; }
+nav a { margin-right: 15px; color: #0066cc; text-decoration: none; }
+nav a:hover { text-decoration: underline; }
+table { width: 100%; border-collapse: collapse; margin-top: 20px; }
+th { text-align: left; padding: 10px; border-bottom: 2px solid #333; font-weight: bold; }
+td { padding: 8px; border-bottom: 1px solid #ddd; vertical-align: top; }
+td.date { white-space: nowrap; width: 150px; color: #666; }
+td.from { width: 250px; }
+td.subject a { color: #0066cc; text-decoration: none; }
+td.subject a:hover { text-decoration: underline; }
+tr:hover { background: #f5f5f5; }