stamail

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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:
MMakefile | 2+-
Mstamail.c | 287++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Astyle.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("&lt;", fp); break; + case '>': fputs("&gt;", fp); break; + case '&': fputs("&amp;", fp); break; + case '"': fputs("&quot;", fp); break; + case '\'': fputs("&#39;", 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; }