stamail

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

stamail.c (19904B)


      1 /*
      2  * ============================================================================
      3  * ███████╗████████╗ █████╗ ███╗   ███╗ █████╗ ██╗██╗     
      4  * ██╔════╝╚══██╔══╝██╔══██╗████╗ ████║██╔══██╗██║██║     
      5  * ███████╗   ██║   ███████║██╔████╔██║███████║██║██║     
      6  * ╚════██║   ██║   ██╔══██║██║╚██╔╝██║██╔══██║██║██║     
      7  * ███████║   ██║   ██║  ██║██║ ╚═╝ ██║██║  ██║██║███████╗
      8  * ╚══════╝   ╚═╝   ╚═╝  ╚═╝╚═╝     ╚═╝╚═╝  ╚═╝╚═╝╚══════╝
      9  * ============================================================================
     10  *
     11  * Copyright (C) 2026 Binkd.
     12  *
     13  * This file is part of stamail.
     14  *
     15  * stamail is free software: you can redistribute it and/or modify it under the
     16  * terms of the GNU General Public License as published by the Free Software
     17  * Foundation, either version 3 of the License, or (at your option) any later
     18  * version.
     19  *
     20  * stamail is distributed in the hope that it will be useful, but WITHOUT ANY
     21  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
     22  * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
     23  * details.
     24  * 
     25  * You should have received a copy of the GNU General Public License
     26  * along with stamail. If not, see <https://www.gnu.org/licenses/>.
     27  *
     28  * =============================================================================
     29 */
     30 
     31 //// Includes
     32 #include <stdio.h>
     33 #include <stdlib.h>
     34 #include <string.h>
     35 #include <time.h>
     36 #include <sys/stat.h>
     37 #include <sys/types.h>
     38 #include <errno.h>
     39 
     40 #include "stamail.h"
     41 #include "sexp.h"
     42 #include "thread.h"
     43 #include "headers.h"
     44 
     45 /* Callback handler */
     46 /*
     47 static void print_message(struct message *m, void *ud) {
     48     (void)ud;
     49 
     50     printf("Message-ID: %.*s\n", m->id_len, m->id);
     51     printf("From:       %.*s\n", m->from_len, m->from);
     52     printf("Subject:    %.*s\n", m->subject_len, m->subject);
     53     printf("Date:       %.*s\n", m->date_len, m->date);
     54     if (m->body) {
     55         printf("Body:\n%.*s\n", m->body_len, m->body);
     56     }
     57     printf("--------------------------------------------------\n");
     58 }
     59 */
     60 
     61 static struct message_node *head = NULL;
     62 static struct message_node *tail = NULL;
     63 static size_t message_count = 0;
     64 
     65 
     66 /* Current callback handler */
     67 static void collect_message(struct message *m, void *ud) {
     68     (void)ud;
     69 
     70     struct message_node *n = malloc(sizeof(struct message_node));
     71     if (!n) {
     72         perror("malloc");
     73         exit(1);
     74     }
     75 
     76     /* shallow copy: pointers still reference JSON buffer */
     77     n->m = *m;
     78     n->next = NULL;
     79 
     80     if (!head) {
     81         head = tail = n;
     82     } else {
     83         tail->next = n;
     84         tail = n;
     85     }
     86     message_count++;
     87 }
     88 
     89 static struct sexp *plist_get(struct sexp *plist, const char *key) {
     90     for (struct sexp *e = plist; e && e->next; e = e->next->next) {
     91         if (e->type == SEXP_SYMBOL &&
     92             e->len == (int)strlen(key) &&
     93             memcmp(e->start, key, e->len) == 0)
     94             return e->next;
     95     }
     96     return NULL;
     97 }
     98 
     99 static void parse_message_plist(struct sexp *plist,
    100                                 struct message *m) {
    101     memset(m, 0, sizeof(*m));
    102 
    103     struct sexp *v;
    104 
    105     if ((v = plist_get(plist, ":id"))) {
    106         m->id = v->start;
    107         m->id_len = v->len;
    108     }
    109     if ((v = plist_get(plist, ":filename"))) {
    110         if (v->type == SEXP_LIST && v->child) {
    111             m->filename = v->child->start;
    112             m->filename_len = v->child->len;
    113         }
    114     }
    115     if ((v = plist_get(plist, ":timestamp"))) {
    116         m->timestamp = strtoul(v->start, NULL, 10);
    117     }
    118 
    119     if ((v = plist_get(plist, ":headers")) && v->type == SEXP_LIST) {
    120         struct sexp *h = v->child;
    121         struct sexp *x;
    122 
    123         if ((x = plist_get(h, ":From"))) {
    124             m->from = x->start;
    125             m->from_len = x->len;
    126         }
    127         if ((x = plist_get(h, ":Subject"))) {
    128             m->subject = x->start;
    129             m->subject_len = x->len;
    130         }
    131         if ((x = plist_get(h, ":Date"))) {
    132             m->date = x->start;
    133             m->date_len = x->len;
    134         }
    135     }
    136 
    137     if ((v = plist_get(plist, ":body")) && v->type == SEXP_LIST) {
    138         struct sexp *b = v->child;
    139         if (b && (b = plist_get(b->child, ":content"))) {
    140             m->body = b->start;
    141             m->body_len = b->len;
    142         }
    143     }
    144 }
    145 
    146 void parse_mail_sexp(const char *sexp, size_t len,
    147                      message_cb cb, void *ud) {
    148     struct sexp *root = sexp_parse(sexp, len);
    149     if (!root) return;
    150 
    151     struct message m;
    152 
    153     /* DFS */
    154     void walk(struct sexp *n) {
    155         if (!n) return;
    156 
    157         if (n->type == SEXP_LIST && n->child &&
    158             n->child->type == SEXP_LIST) {
    159 
    160             struct sexp *plist = n->child->child;
    161             struct sexp *id = plist_get(plist, ":id");
    162             struct sexp *headers = plist_get(plist, ":headers");
    163 
    164             if (id && headers && headers->type == SEXP_LIST) {
    165                 parse_message_plist(plist, &m);
    166 
    167                 /* sanity: must have a non-empty message-id */
    168                 if (m.id && m.id_len > 1) {
    169                     cb(&m, ud);
    170                 }
    171             }
    172         }
    173 
    174         walk(n->child);
    175         walk(n->next);
    176     }
    177 
    178     walk(root);
    179     sexp_free(root);
    180 }
    181 
    182 
    183 
    184 /* HTML Escape Function */
    185 static void fprinthtml(FILE *fp, const char *s, int len) {
    186     if (!s || len <= 0)
    187         return;
    188     
    189     for (int i = 0; i < len; i++) {
    190         /* Handle escaped newlines: \n */
    191         if (s[i] == '\\' && i + 1 < len && s[i + 1] == 'n') {
    192             fputs("<br/>\n", fp);
    193             i++; /* skip 'n' */
    194             continue;
    195         }
    196 
    197         /* Handle escaped tabs */
    198         if (s[i] == '\\' && i + 1 < len && s[i + 1] == 't') {
    199             fputs("&nbsp;&nbsp;&nbsp;&nbsp;", fp);
    200             i++;
    201             continue;
    202         }
    203         switch (s[i]) {
    204         case '<':  fputs("&lt;", fp);   break;
    205         case '>':  fputs("&gt;", fp);   break;
    206         case '&':  fputs("&amp;", fp);  break;
    207         case '"':  fputs("&quot;", fp); break;
    208         case '\'': fputs("&#39;", fp);  break;
    209         case '\n': fputs("<br/>\n", fp); break;
    210         case '\r': /* ignore */          break;
    211         default:   fputc(s[i], fp);     break;
    212         }
    213     }
    214 }
    215 
    216 /* Simple hash of message-id for filename */
    217 static unsigned long hash_msgid(const char *s, int len) {
    218     unsigned long hash = 5381;
    219     for (int i = 0; i < len; i++)
    220         hash = ((hash << 5) + hash) + (unsigned char)s[i];
    221     return hash;
    222 }
    223 
    224 /* Write HTML header */
    225 static void writeheader(FILE *fp, const char *title) {
    226     fprintf(fp, "<!DOCTYPE html>\n");
    227     fprintf(fp, "<html>\n");
    228     fprintf(fp, "<head>\n");
    229     fprintf(fp, "<meta charset=\"UTF-8\">\n");
    230     fprintf(fp, "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n");
    231     fprintf(fp, "<title>%s</title>\n", title);
    232     fprintf(fp, "<link rel=\"stylesheet\" href=\"style.css\">\n");
    233     fprintf(fp, "</head>\n");
    234     fprintf(fp, "<body>\n");
    235 }
    236 
    237 /* Write HTML footer */
    238 static void writefooter(FILE *fp) {
    239     fprintf(fp, "</body>\n");
    240     fprintf(fp, "</html>\n");
    241 }
    242 
    243 /* Create output directory if it doesn't exist */
    244 static int ensure_dir(const char *path) {
    245     struct stat st;
    246     if (stat(path, &st) == 0)
    247         return 0;  /* Already exists */
    248     
    249     if (mkdir(path, 0755) != 0 && errno != EEXIST) {
    250         perror(path);
    251         return -1;
    252     }
    253     return 0;
    254 }
    255 
    256 /* Copy style.css from source to output directory */
    257 static int copy_stylesheet(const char *source, const char *output_dir) {
    258     FILE *src, *dst;
    259     char dst_path[1024];
    260     char buf[4096];
    261     size_t n;
    262     
    263     /* Open source file */
    264     src = fopen(source, "r");
    265     if (!src) {
    266         perror(source);
    267         return -1;
    268     }
    269     
    270     /* Open destination file */
    271     snprintf(dst_path, sizeof(dst_path), "%s/style.css", output_dir);
    272     dst = fopen(dst_path, "w");
    273     if (!dst) {
    274         perror(dst_path);
    275         fclose(src);
    276         return -1;
    277     }
    278     
    279     /* Copy contents */
    280     while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
    281         if (fwrite(buf, 1, n, dst) != n) {
    282             perror("fwrite");
    283             fclose(src);
    284             fclose(dst);
    285             return -1;
    286         }
    287     }
    288     
    289     fclose(src);
    290     fclose(dst);
    291     return 0;
    292 }
    293 
    294 /* Write index.html */
    295 static int write_index(const char *output_dir, const char *archive_name) {
    296     FILE *fp;
    297     char path[1024];
    298     
    299     snprintf(path, sizeof(path), "%s/index.html", output_dir);
    300     
    301     fp = fopen(path, "w");
    302     if (!fp) {
    303         perror(path);
    304         return -1;
    305     }
    306     
    307     writeheader(fp, archive_name);
    308     
    309     fprintf(fp, "<h1>%s</h1>\n", archive_name);
    310     fprintf(fp, "<nav>\n");
    311     fprintf(fp, "<a href=\"index.html\">Messages</a> | ");
    312     fprintf(fp, "<a href=\"threads.html\">Threads</a>\n");
    313     fprintf(fp, "</nav>\n");
    314     
    315     fprintf(fp, "<p>%zu messages</p>\n", message_count);
    316     
    317     fprintf(fp, "<table>\n");
    318     fprintf(fp, "<thead>\n");
    319     fprintf(fp, "<tr><th>Date</th><th>From</th><th>Subject</th></tr>\n");
    320     fprintf(fp, "</thead>\n");
    321     fprintf(fp, "<tbody>\n");
    322     
    323     for (struct message_node *n = head; n; n = n->next) {
    324         struct message *m = &n->m;
    325         unsigned long hash = hash_msgid(m->id, m->id_len);
    326         
    327         fprintf(fp, "<tr>");
    328         
    329         /* Date */
    330         fprintf(fp, "<td class=\"date\">");
    331         fprinthtml(fp, m->date, m->date_len);
    332         fprintf(fp, "</td>");
    333         
    334         /* From */
    335         fprintf(fp, "<td class=\"from\">");
    336         fprinthtml(fp, m->from, m->from_len);
    337         fprintf(fp, "</td>");
    338         
    339         /* Subject (with link to message page) */
    340         fprintf(fp, "<td class=\"subject\">");
    341         fprintf(fp, "<a href=\"msg-%08lx.html\">", hash);
    342         fprinthtml(fp, m->subject, m->subject_len);
    343         fprintf(fp, "</a>");
    344         fprintf(fp, "</td>");
    345         
    346         fprintf(fp, "</tr>\n");
    347     }
    348     
    349     fprintf(fp, "</tbody>\n");
    350     fprintf(fp, "</table>\n");
    351     
    352     writefooter(fp);
    353     fclose(fp);
    354     return 0;
    355 }
    356 
    357 /* Write individual message page */
    358 static int write_message(const char *output_dir, struct message *m) {
    359     FILE *fp;
    360     char path[1024];
    361     unsigned long hash = hash_msgid(m->id, m->id_len);
    362     
    363     snprintf(path, sizeof(path), "%s/msg-%08lx.html", output_dir, hash);
    364     
    365     fp = fopen(path, "w");
    366     if (!fp) {
    367         perror(path);
    368         return -1;
    369     }
    370     
    371     /* Title from subject */
    372     writeheader(fp, "Message");
    373     
    374     fprintf(fp, "<nav>\n");
    375     fprintf(fp, "<a href=\"index.html\">← Back to index</a>\n");
    376     fprintf(fp, "</nav>\n");
    377     
    378     fprintf(fp, "<div style=\"margin: 20px 0; padding: 15px; background: #f5f5f5; border-left: 3px solid #666;\">\n");
    379     
    380     /* From */
    381     fprintf(fp, "<div><strong>From:</strong> ");
    382     fprinthtml(fp, m->from, m->from_len);
    383     fprintf(fp, "</div>\n");
    384     
    385     /* Date */
    386     fprintf(fp, "<div><strong>Date:</strong> ");
    387     fprinthtml(fp, m->date, m->date_len);
    388     fprintf(fp, "</div>\n");
    389     
    390     /* Subject */
    391     fprintf(fp, "<div><strong>Subject:</strong> ");
    392     fprinthtml(fp, m->subject, m->subject_len);
    393     fprintf(fp, "</div>\n");
    394     
    395     /* Message-ID */
    396     fprintf(fp, "<div style=\"font-size: 0.9em; color: #666;\"><strong>Message-ID:</strong> ");
    397     fprinthtml(fp, m->id, m->id_len);
    398     fprintf(fp, "</div>\n");
    399     
    400     fprintf(fp, "</div>\n");
    401     
    402     /* Body */
    403     if (m->body && m->body_len > 0) {
    404         fprintf(fp, "<pre style=\"white-space: pre-wrap; font-family: monospace; padding: 15px; background: #fafafa; border: 1px solid #ddd;\">");
    405         fprinthtml(fp, m->body, m->body_len);
    406         fprintf(fp, "</pre>\n");
    407     }
    408     
    409     writefooter(fp);
    410     fclose(fp);
    411     return 0;
    412 }
    413 
    414 static void render_message_line(FILE *fp,
    415                                 struct message *m) {
    416     unsigned long hash = hash_msgid(m->id, m->id_len);
    417 
    418     /* subject link */
    419     fprintf(fp, "<a href=\"msg-%08lx.html\">", hash);
    420     fprinthtml(fp, m->subject, m->subject_len);
    421     fprinthtml(fp, " <", 2);
    422     fprinthtml(fp, m->from, m->from_len);
    423     fprinthtml(fp, ">", 1);
    424     fprintf(fp, "</a>");
    425 
    426     /* inline details for body */
    427     if (m->body && m->body_len > 0) {
    428         fprintf(fp, " ");
    429         fprintf(fp, "<details>");
    430         fprintf(fp, "<summary>+</summary>");
    431         fprintf(fp, "<div class=\"body\">");
    432         fprinthtml(fp, m->body, m->body_len);
    433         fprintf(fp, "</div>");
    434         fprintf(fp, "</details>");
    435     }
    436 }
    437 
    438 static void render_thread_ascii(FILE *fp,
    439                                 struct thread_node *n,
    440                                 const char *prefix,
    441                                 int is_last) {
    442     char next_prefix[1024];
    443 
    444     fprintf(fp, "%s%s ",
    445             prefix,
    446             is_last ? "└─" : "├─");
    447 
    448     render_message_line(fp, n->msg);
    449     fputc('\n', fp);
    450 
    451     snprintf(next_prefix, sizeof(next_prefix),
    452              "%s%s",
    453              prefix,
    454              is_last ? "  " : "│ ");
    455 
    456     for (struct thread_node *c = n->child; c; c = c->sibling) {
    457         render_thread_ascii(fp,
    458                             c,
    459                             next_prefix,
    460                             c->sibling == NULL);
    461     }
    462 }
    463 
    464 
    465 static int write_threads(const char *output_dir,
    466                           const char *archive_name,
    467                           struct thread_node **threads,
    468                           int num_threads)
    469 {
    470     FILE *fp;
    471     char path[1024];
    472 
    473     snprintf(path, sizeof(path), "%s/threads.html", output_dir);
    474     fp = fopen(path, "w");
    475     if (!fp) {
    476         perror(path);
    477         return -1;
    478     }
    479 
    480     writeheader(fp, archive_name);
    481 
    482     fprintf(fp, "<h1>%s — Threads</h1>\n", archive_name);
    483     fprintf(fp, "<nav>\n");
    484     fprintf(fp, "<a href=\"index.html\">Messages</a> | ");
    485     fprintf(fp, "<a href=\"threads.html\">Threads</a>\n");
    486     fprintf(fp, "</nav>\n");
    487 
    488     fprintf(fp,
    489         "<pre class=\"threads\">\n");
    490 
    491     for (int i = 0; i < num_threads; i++) {
    492         struct thread_node *r = *threads++;
    493 
    494         /* root line (no prefix) */
    495         unsigned long hash =
    496             hash_msgid(r->msg->id, r->msg->id_len);
    497 
    498         fprintf(fp, "<details open class=\"threads\">\n");
    499         fprintf(fp, "<summary>");
    500 
    501         /* root rendered as tree head */
    502         fprintf(fp, "└─ ");
    503         render_message_line(fp, r->msg);
    504 
    505         fprintf(fp, "</summary>\n");
    506 
    507         /* children share the same tree */
    508         for (struct thread_node *c = r->child; c; c = c->sibling) {
    509             render_thread_ascii(fp,
    510                                 c,
    511                                 "   ",
    512                                 c->sibling == NULL);
    513         }
    514 
    515         fprintf(fp, "</details>\n");
    516 
    517 
    518         fprintf(fp, "\n");
    519     }
    520 
    521     fprintf(fp, "</pre>\n");
    522     writefooter(fp);
    523     fclose(fp);
    524     return 0;
    525 }
    526 
    527 int main(int argc, char *argv[]) {
    528     /* CONFIG */
    529     const char *output_dir = "./output";
    530     const char *archive_name = "Mail Archive";
    531     const char *stylesheet = "./style.css";
    532 
    533      /* Simple argument parsing */
    534     for (int i = 1; i < argc; i++) {
    535         if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
    536             output_dir = argv[++i];
    537         } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
    538             archive_name = argv[++i];
    539         } else if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) {
    540             stylesheet = argv[++i];
    541         } else if (strcmp(argv[i], "-h") == 0) {
    542             printf("usage: stamail [-o output_dir] [-n archive_name] [-s style.css]\n");
    543             printf("  -o  output directory (default: .)\n");
    544             printf("  -n  archive name\n");
    545             printf("  -s  path to style.css to copy\n");
    546             return 0;
    547         }
    548     }
    549 
    550     /* Slurp stdin */
    551     char *buf = NULL;
    552     size_t cap = 0, len = 0;
    553     int c;
    554 
    555     while ((c = getchar()) != EOF) {
    556         if (len + 1 >= cap) {
    557             cap = cap ? cap * 2 : 4096;
    558             buf = realloc(buf, cap);
    559             if (!buf) {
    560                 perror("realloc");
    561                 return 1;
    562             }
    563         }
    564         buf[len++] = c;
    565     }
    566 
    567     if (!buf) {
    568         fprintf(stderr, "No input\n");
    569         return 1;
    570     }
    571 
    572     parse_mail_sexp(buf, len, collect_message, NULL);
    573 
    574     /* Debug: print what we got from JSON */
    575     for (struct message_node *n = head; n; n = n->next) {
    576         struct message *m = &n->m;
    577         printf("MSG: %.*s\n", m->subject_len, m->subject);
    578         printf("  ID: %.*s\n", m->id_len, m->id);
    579         if (m->in_reply_to_len > 0) {
    580             printf("  IRT: %.*s\n", m->in_reply_to_len, m->in_reply_to);
    581         } else {
    582             printf("  IRT: (none)\n");
    583         }
    584         printf("\n");
    585     }
    586 
    587     for (struct message_node *n = head; n; n = n->next) {
    588         struct message *m = &n->m;
    589 
    590         printf("Message-ID: %.*s\n", m->id_len, m->id);
    591         printf("From:       %.*s\n", m->from_len, m->from);
    592         printf("Subject:    %.*s\n", m->subject_len, m->subject);
    593         printf("Date:       %.*s\n", m->date_len, m->date);
    594         if (m->body) {
    595             printf("Body:\n%.*s\n", m->body_len, m->body);
    596         }
    597         printf("--------------------------------------------------\n");
    598     }
    599 
    600     if (message_count == 0) {
    601         fprintf(stderr, "No messages found\n");
    602         free(buf);
    603         return 1;
    604     }
    605 
    606     /* Extract threading headers from maildir files */
    607     fprintf(stderr, "Reading threading headers from %zu messages...\n", message_count);
    608     for (struct message_node *n = head; n; n = n->next) {
    609         extract_threading_headers(n);
    610     }
    611 
    612     /* Debug: print what we got */
    613     for (struct message_node *n = head; n; n = n->next) {
    614         struct message *m = &n->m;
    615         printf("MSG: %.*s\n", m->subject_len, m->subject);
    616         printf("  ID: %.*s\n", m->id_len, m->id);
    617         if (m->in_reply_to_len > 0) {
    618             printf("  IRT: %.*s\n", m->in_reply_to_len, m->in_reply_to);
    619         } else {
    620             printf("  IRT: (none)\n");
    621         }
    622         printf("\n");
    623     }
    624     
    625     /* Create output directory */
    626     if (ensure_dir(output_dir) != 0) {
    627         free(buf);
    628         return 1;
    629     }
    630     
    631     /* Generate HTML files */
    632     fprintf(stderr, "Generating HTML for %zu messages...\n", message_count);
    633     
    634     /* Copy stylesheet if provided */
    635     if (stylesheet) {
    636         if (copy_stylesheet(stylesheet, output_dir) != 0) {
    637             fprintf(stderr, "Warning: failed to copy stylesheet\n");
    638         }
    639     }
    640     
    641     if (write_index(output_dir, archive_name) != 0) {
    642         free(buf);
    643         return 1;
    644     }
    645     
    646     /* Write individual message pages */
    647     for (struct message_node *n = head; n; n = n->next) {
    648         if (write_message(output_dir, &n->m) != 0) {
    649             free(buf);
    650             return 1;
    651         }
    652     }
    653     
    654     fprintf(stderr, "Done! Output written to %s/\n", output_dir);
    655 
    656     /* Build thread tree */
    657     int num_threads = 0;
    658     struct thread_node **threads = build_threads(head, &num_threads);
    659 
    660     if (threads) {
    661         printf("Debug print of threading");
    662         print_threads(threads, num_threads);
    663     }
    664 
    665     if (threads) {
    666         if (write_threads(output_dir,
    667                           archive_name,
    668                           threads,
    669                           num_threads) != 0) {
    670             fprintf(stderr, "Failed to write threads.html\n");
    671         }
    672     }
    673 
    674     free(buf);
    675     struct message_node *n = head;
    676     while (n) {
    677         struct message_node *next = n->next;
    678         free(n);
    679         n = next;
    680     }
    681 
    682     return 0;
    683 }
    684