summary refs log blame commit diff
path: root/html.c
blob: 77438a0fde4d7838c38fc060e9aa50aaef1aab36 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14












                                                                         


                                                                 
                                                                     


                                                                       
                                                                   
                
   
                   
                
                  
                    






                     







                                                                          
                                                                     
                                        
                             
                        
                                                               
                                  
                                         
                
                               
         
                                  
                                     
                                              
                                              
                    

                                                                    
 
          
                                                                        
                                

                                    
          
                                  
                                 
                    
                                                                    
                                               

                                                                            
                                        
                                                           
 
                                                  
                                  
                                           
                    
                                                               
 
 
                                              
                                  
                                           
                                   
                    
                                                                   
 













                                                                            
                                                         


                                                   
                                         
                                        
                                                
                      
          
                                  
                                                         



                                                                                
                    





                                                                  
 
                                                                 

                                                   
                                                   
                                          















                                                                    
                   
                     
 
                                                                  
                                                           
                                                  



                                                                                   
                                                                   

                                                                    
                                  
                                                     
                                         
                                                 
                                     
                                                        
                                                     
                                           
                    
                     
                                                                  
                                                            
                                                 
                                                                  
                       
                     
                     
 
 





                                                                  





                                                  
                             
                                                                        










                                                                                    
                                                                    










                                                                        
                                                   
                           
                                                                                







                                                                    
 
                                                        

                         
                                                
                                                    





                                                           
 




                                                                        
                 








                                                                        
                                                       

                                                                             
                                                      
                                                                             

                                                                                
                                           
                                                           
                        
                                                          
                 
 
                                                                        
                                 

                                        



                     
                                                                              















                                                                                
                
                                                                  
                                            
                                                               
 
                                    
                                                                            
 
                   
                                                                            
   
                                                                           
                                                                          
                                                         

                                                                      
                                  


                                                           





                                                                    
                                     
                                                          
 
                                  
                                                               
 
 
















                                                                                
                                                                               
                                  
                                                     
                    
                                                                  

                                                                 
                                                     





                                                                                
                                                                                      
          
                                  
                                               
                                                 
                                 
                    

                                                                  
                   

                     
                                                                 
                                                     


                                          

                                                                 


                                     
                                                 
                                 
                    
                                                                    
                   
                     
 
                                                 








                                                                 


                                                                                  
                                  
                                                        

                                                               
 









                                                                   
                                    
                                                               
 
                                   




                                                             
                                  
                                               
                                                  

                                                               
 

                                 
                                                               


                                    




                                                                                    
                                                                                      
          
                                  
                                               
                                       
                    

                                                                  

                               
                                 
                                        



                                                                   
                                         
                                                                   
                                   
                              


                                    
                                  
                                       
                                               
                    
                                                               






                                                            
                                                            



                                                                       
                                           






                                                                              


                                                        
                                                     
                                           
                    


                                                                  




                                
                                                                    
                                    
/* 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(&regex, "(^|[[:space:]<])(https?:[^[:space:]>]+)(.|$)");

	int error;
	char *ptr;
	regmatch_t match[4];
	for (ptr = buf; !regexec(&regex, 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 htmlFooter(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);
}