/* 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/>.
*
* Additional permission under GNU GPL version 3 section 7:
*
* If you modify this Program, or any covered work, by linking or
* combining it with OpenSSL (or a modified version of that library),
* containing parts covered by the terms of the OpenSSL License and the
* original SSLeay license, the licensors of this Program grant you
* additional permission to convey the resulting work. Corresponding
* Source for a non-source form of such a combination shall include the
* source code for the parts of OpenSSL used as well as that of the
* covered work.
*/
#include <assert.h>
#include <err.h>
#include <regex.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <time.h>
#include "archive.h"
static char *htmlMailto(struct Address addr) {
struct Variable vars[] = {
{ "mailbox", addr.mailbox },
{ "host", addr.host },
{0},
};
return templateString("mailto:[mailbox]@[host]", vars, escapeURL);
}
static int htmlAddress(FILE *file, struct Address addr, bool comma) {
char *mailto = htmlMailto(addr);
const char *template;
if (addr.host) {
template = Q([,]<a href="[mailto]">[name]</a>);
} else if (addr.mailbox) {
template = "[mailbox]: ";
} else {
template = ";";
}
struct Variable vars[] = {
{ "mailto", mailto },
{ "name", addressName(addr) },
{ "mailbox", addr.mailbox },
{ ",", (comma ? ", " : " ") },
{0},
};
int error = templateRender(file, template, vars, escapeXML);
free(mailto);
return error;
}
static int
htmlAddressList(FILE *file, const char *name, struct AddressList list) {
if (!list.len) return 0;
const char *template = Q(
<div class="[name]">
[name]:
);
struct Variable vars[] = {
{ "name", name },
{0},
};
int error = templateRender(file, template, vars, escapeXML);
if (error) return error;
for (size_t i = 0; i < list.len; ++i) {
error = htmlAddress(
file, list.addrs[i], i > 0 && list.addrs[i - 1].host
);
if (error) return error;
}
return templateRender(file, Q(</div>), NULL, NULL);
}
static char *htmlFragment(const char *messageID) {
struct Variable vars[] = {
{ "messageID", messageID },
{0},
};
return templateString("#[messageID]", vars, escapeURL);
}
static char *htmlMbox(const char *messageID) {
struct Variable vars[] = {
{ "messageID", messageID },
{ "type", "mbox" },
{0},
};
return templateString("../" PATH_MESSAGE, vars, escapeURL);
}
static int htmlReplyCc(FILE *file, bool first, struct AddressList list) {
for (size_t i = 0; i < list.len; ++i) {
const char *template = "[,][mailbox]@[host]";
struct Variable vars[] = {
{ "mailbox", list.addrs[i].mailbox },
{ "host", list.addrs[i].host },
{ ",", (!first || i ? "," : "") },
{0},
};
int error = templateRender(file, template, vars, escapeURL);
if (error) return error;
}
return 0;
}
static char *htmlReply(const struct Envelope *envelope) {
char *buf;
size_t len;
FILE *file = open_memstream(&buf, &len);
if (!file) err(EX_OSERR, "open_memstream");
const char *template = {
"mailto:[mailbox]@[host]"
"?subject=[re][subject]"
"&In-Reply-To=[<][messageID][>]"
"&cc="
};
struct Variable vars[] = {
{ "mailbox", envelope->replyTo.mailbox },
{ "host", envelope->replyTo.host },
{ "re", (strncmp(envelope->subject, "Re: ", 4) ? "Re: " : "") },
{ "subject", envelope->subject },
{ "messageID", envelope->messageID },
{ "<", "<" },
{ ">", ">" },
{0},
};
int error = 0
|| templateRender(file, template, vars, escapeURL)
|| htmlReplyCc(file, true, envelope->to)
|| htmlReplyCc(file, false, envelope->cc)
|| fclose(file);
if (error) err(EX_OSERR, "open_memstream");
return buf;
}
int htmlMessageNav(FILE *file, const struct Envelope *envelope) {
char *parent = envelope->inReplyTo
? htmlFragment(envelope->inReplyTo)
: NULL;
char *mbox = htmlMbox(envelope->messageID);
char *reply = htmlReply(envelope);
const char *template = Q(
<nav>
[+parent]
<a href="[parent]">parent</a>
[-]
<a href="[mbox]">download</a>
<a href="[reply]">reply</a>
</nav>
);
struct Variable vars[] = {
{ "parent", parent },
{ "mbox", mbox },
{ "reply", reply },
{0},
};
int error = templateRender(file, template, vars, escapeXML);
free(parent);
free(mbox);
free(reply);
return error;
}
int htmlMessageOpen(FILE *file, const struct Envelope *envelope) {
char *fragment = htmlFragment(envelope->messageID);
char *mailto = htmlMailto(envelope->from);
const char *template = Q(
<article class="message" id="[messageID]">
<header>
<h2 class="Subject"><a href="[fragment]">[subject]</a></h2>
<div class="From">
From: <a href="[mailto]">[from]</a>
<time datetime="[utc]">[date]</time>
</div>
);
struct Variable vars[] = {
{ "messageID", envelope->messageID },
{ "fragment", fragment },
{ "subject", envelope->subject },
{ "mailto", mailto },
{ "from", addressName(envelope->from) },
{ "utc", iso8601(envelope->time).s },
{ "date", envelope->date },
{0},
};
int error = 0
|| templateRender(file, template, vars, escapeXML)
|| htmlAddressList(file, "To", envelope->to)
|| htmlAddressList(file, "Cc", envelope->cc)
|| htmlMessageNav(file, envelope)
|| templateRender(file, Q(</header>), NULL, NULL);
free(fragment);
free(mailto);
return error;
}
static void compile(regex_t *regex, const char *pattern) {
if (!regex->re_nsub) {
int error = regcomp(regex, pattern, REG_EXTENDED);
assert(!error);
}
}
static void swap(char *a, char *b) {
char ch = *a;
*a = *b;
*b = ch;
}
static int htmlMarkupURLs(FILE *file, char *buf) {
static regex_t regex;
compile(®ex, "(^|[[:space:]<])(https?:[^[:space:]>]+)(.|$)");
int error;
char *ptr;
regmatch_t match[4];
for (ptr = buf; !regexec(®ex, ptr, 4, match, 0); ptr += match[2].rm_eo) {
char nul = '\0';
swap(&ptr[match[2].rm_so], &nul);
error = escapeXML(file, ptr);
if (error) return error;
swap(&ptr[match[2].rm_so], &nul);
const char *template = Q(<a href="[url]">[url]</a>);
swap(&ptr[match[3].rm_so], &nul);
struct Variable vars[] = {
{ "url", &ptr[match[2].rm_so] },
{0},
};
error = templateRender(file, template, vars, escapeXML);
if (error) return error;
swap(&ptr[match[3].rm_so], &nul);
}
return escapeXML(file, ptr);
}
static int htmlMarkupQuote(FILE *file, char *buf) {
uint32_t level = 0;
for (char *ch = buf; *ch == '>' || *ch == ' '; level += (*ch++ == '>'));
const char *template = Q(<span class="quote level[level]">);
struct Variable vars[] = {
{ "level", u32(level).s },
{0},
};
return 0
|| templateRender(file, template, vars, escapeXML)
|| htmlMarkupURLs(file, buf)
|| templateRender(file, Q(</span>), NULL, NULL);
}
static int htmlMarkup(FILE *file, const char *content) {
int error = 0;
size_t cap = 0;
char *buf = NULL;
enum { Init, Patch, Diff } state = Init;
while (*content) {
size_t len = strcspn(content, "\n");
if (cap < len + 1) {
cap = len + 1;
buf = realloc(buf, cap);
if (!buf) err(EX_OSERR, "realloc");
}
memcpy(buf, content, len);
buf[len] = '\0';
if (state == Init && !strcmp(buf, "---")) {
state = Patch;
} else if (state == Patch && !strncmp(buf, "diff", 4)) {
state = Diff;
} else if (!strcmp(buf, "-- ")) {
state = Init;
}
if (state == Diff) {
const char *template = Q(
<span class="diff [class]">[line]</span>
);
struct Variable vars[] = {
{ "class", "head" },
{ "line", buf },
{0},
};
if (buf[0] == '@') {
vars[0].value = "hunk";
} else if (buf[0] == ' ') {
vars[0].value = "ctx";
} else if (buf[0] == '-' && strncmp(buf, "---", 3)) {
vars[0].value = "old";
} else if (buf[0] == '+' && strncmp(buf, "+++", 3)) {
vars[0].value = "new";
}
error = templateRender(file, template, vars, escapeXML);
} else if (buf[0] == '>') {
error = htmlMarkupQuote(file, buf);
} else {
error = htmlMarkupURLs(file, buf);
}
error = error || templateRender(file, "\n", NULL, NULL);
if (error) break;
content += len;
if (*content) content++;
}
free(buf);
return error;
}
int htmlInline(FILE *file, const struct BodyPart *part, const char *content) {
const char *template = Q(
<pre
[+contentID]id="[contentID]"[-]
[+description]title="[description]"[-]
[+language]lang="[language]"[-]>
);
const char *language = NULL;
if (part->language.type == String) language = part->language.string;
if (part->language.type == List && part->language.list.len == 1) {
language = dataCheck(part->language.list.ptr[0], String).string;
}
struct Variable vars[] = {
{ "contentID", part->contentID },
{ "description", part->description },
{ "language", language },
{0},
};
return 0
|| templateRender(file, template, vars, escapeXML)
|| htmlMarkup(file, content)
|| templateRender(file, Q(</pre>), NULL, NULL);
}
int htmlAttachmentOpen(FILE *file) {
return templateRender(file, Q(<ul class="attachment">), NULL, NULL);
}
int htmlAttachment(
FILE *file, const struct BodyPart *part, const struct Variable *path
) {
char *url = templateString("../" PATH_ATTACHMENT, path, escapeURL);
const char *name = paramGet(part->disposition.params, "filename");
if (!name) name = paramGet(part->params, "name");
const char *template = Q(
<li><a href="[url]">[name][type][/][subtype]</a> </li>
);
struct Variable vars[] = {
{ "url", url },
{ "name", (name ? name : "") },
{ "type", (name ? "" : part->type) },
{ "/", (name ? "" : "/") },
{ "subtype", (name ? "" : part->subtype) },
{0},
};
int error = templateRender(file, template, vars, escapeXML);
free(url);
return error;
}
int htmlAttachmentClose(FILE *file) {
return templateRender(file, Q(</ul>), NULL, NULL);
}
int htmlMessageClose(FILE *file) {
return templateRender(file, Q(</article>), NULL, NULL);
}
static int htmlStylesheet(FILE *file) {
if (baseStylesheet) {
const char *template = Q(<link rel="stylesheet" href="[href]">);
struct Variable vars[] = {
{ "href", baseStylesheet },
{0},
};
return templateRender(file, template, vars, escapeXML);
} else {
const char *template = Q(<style>[style]</style>);
struct Variable vars[] = {
{ "style", Stylesheet },
{0},
};
return templateRender(file, template, vars, NULL);
}
}
static char *htmlThreadURL(const struct Envelope *envelope, const char *type) {
struct Variable vars[] = {
{ "messageID", envelope->messageID },
{ "type", type },
{0},
};
return templateString("../" PATH_THREAD, vars, escapeURL);
}
int htmlThreadHead(FILE *file, const struct Envelope *envelope) {
char *atom = htmlThreadURL(envelope, "atom");
char *mbox = htmlThreadURL(envelope, "mbox");
const char *template = Q(
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="generator" content="[generator]">
<title>[subject]</title>
<link rel="alternate" type="application/atom+xml" href="[atom]">
<link rel="alternate" type="application/mbox" href="[mbox]">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
);
struct Variable vars[] = {
{ "generator", GENERATOR_URL },
{ "subject", envelope->subject },
{ "atom", atom },
{ "mbox", mbox },
{0},
};
int error = 0
|| templateRender(file, template, vars, escapeXML)
|| htmlStylesheet(file);
free(atom);
free(mbox);
return error;
}
int htmlThreadOpen(FILE *file, const struct Envelope *envelope) {
char *atom = htmlThreadURL(envelope, "atom");
char *mbox = htmlThreadURL(envelope, "mbox");
const char *template = Q(
<header class="thread">
<h1>[subject]</h1>
<nav>
<a href="../index.html">index</a>
<a href="[atom]">follow</a>
<a href="[mbox]">download</a>
</nav>
</header>
<main class="thread">
);
struct Variable vars[] = {
{ "subject", envelope->subject },
{ "atom", atom },
{ "mbox", mbox },
{0},
};
int error = templateRender(file, template, vars, escapeXML);
free(atom);
free(mbox);
return error;
}
static uint32_t threadCount(struct List thread) {
uint32_t count = 0;
for (size_t i = 0; i < thread.len; ++i) {
if (thread.ptr[i].type == List) {
count += threadCount(thread.ptr[i].list);
} else {
count++;
}
}
return count;
}
static int htmlReplies(FILE *file, uint32_t replies) {
const char *template = Q(
<data class="replies" value="[replies]">[replies] repl[ies]</data>
);
struct Variable vars[] = {
{ "replies", u32(replies).s },
{ "ies", (replies != 1 ? "ies" : "y") },
{0},
};
return templateRender(file, template, vars, escapeXML);
}
int htmlSubthreadOpen(FILE *file, struct List thread) {
const char *template = Q(
<details class="subthread" open>
<summary>
);
return 0
|| templateRender(file, template, NULL, NULL)
|| htmlReplies(file, threadCount(thread))
|| templateRender(file, Q(</summary>), NULL, NULL);
}
int htmlSubthreadClose(FILE *file) {
return templateRender(file, Q(</details>), NULL, NULL);
}
static int (FILE *file) {
const char *template = Q(
<footer>
<a href="[generator]">generated</a>
<time datetime="[time]">[time]</time>
</footer>
);
struct Variable vars[] = {
{ "generator", GENERATOR_URL },
{ "time", iso8601(time(NULL)).s },
{0},
};
return templateRender(file, template, vars, escapeXML);
}
int htmlThreadClose(FILE *file) {
return 0
|| templateRender(file, Q(</main>), NULL, NULL)
|| htmlFooter(file);
}
int htmlIndexHead(FILE *file) {
const char *template = Q(
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="generator" content="[generator]">
<title>[title]</title>
<link rel="alternate" type="application/atom+xml" href="index.atom">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
);
struct Variable vars[] = {
{ "generator", GENERATOR_URL },
{ "title", baseTitle },
{0},
};
return 0
|| templateRender(file, template, vars, escapeXML)
|| htmlStylesheet(file);
}
int htmlIndexOpen(FILE *file) {
const char *template = Q(
<header class="index">
<h1>[title]</h1>
<nav>
<a href="index.atom">follow</a>
[+subscribe]
<a href="[subscribe]">subscribe</a>
[-]
[+mailto]
<a href="mailto:[mailto]">write</a>
[-]
</nav>
</header>
<main class="index">
<ol>
);
struct Variable vars[] = {
{ "title", baseTitle },
{ "subscribe", baseSubscribe },
{ "mailto", baseMailto },
{0},
};
return templateRender(file, template, vars, escapeXML);
}
static char *htmlIndexURL(const struct Envelope *envelope) {
struct Variable vars[] = {
{ "messageID", envelope->messageID },
{ "type", "html" },
{0},
};
return templateString(PATH_THREAD, vars, escapeURL);
}
int htmlIndexThread(
FILE *file, const struct Envelope *envelope, struct List thread
) {
char *url = htmlIndexURL(envelope);
const char *template = Q(
<li>
<h2 class="Subject"><a href="[url]">[subject]</a></h2>
<div class="From">
From: [from]
<time datetime="[utc]">[date]</time>
</div>
);
struct Variable vars[] = {
{ "url", url },
{ "subject", envelope->subject },
{ "from", addressName(envelope->from) },
{ "utc", iso8601(envelope->time).s },
{ "date", envelope->date },
{0},
};
int error = 0
|| templateRender(file, template, vars, escapeXML)
|| htmlReplies(file, threadCount(thread) - 1)
|| templateRender(file, Q(</li>), NULL, NULL);
free(url);
return error;
}
int htmlIndexClose(FILE *file) {
return 0
|| templateRender(file, Q(</ol></main>), NULL, NULL)
|| htmlFooter(file);
}