crabmail

Static HTML email archive viewer in Rust
git clone git://git.alexwennerberg.com/crabmail
Log | Files | Refs | README | LICENSE

commit 272d09602df051a963f3ff89cfd12b3c9c5d5f6f
parent 78d2c8188b6501f7a8ffde75e23869cef24a9e48
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 12 Dec 2021 17:33:15 -0800

some frontend improvements

Diffstat:
Mcrabmail/src/filters.rs | 35++++++++++-------------------------
Mcrabmail/src/main.rs | 47++++++++++++++++++++++++++++++++++++++++++++---
Mcrabmail/templates/base.html | 4++--
Mcrabmail/templates/static/style.css | 20+++++++++++++++++++-
Mcrabmail/templates/thread.html | 9+++++----
5 files changed, 80 insertions(+), 35 deletions(-)

diff --git a/crabmail/src/filters.rs b/crabmail/src/filters.rs @@ -39,30 +39,15 @@ fn timeago(unixtime: u64) -> String { _ => format!("{} {}s ago", amount, metric), } } -// // NOTE this function is currently unsafe -// pub fn get_body(email: &&ParsedMail) -> askama::Result<String> { -// let core_email = email.subparts.get(0).unwrap_or(email); +pub fn get_body(email: &&ParsedMail) -> askama::Result<String> { + let core_email = email.subparts.get(0).unwrap_or(email); -// #[cfg(feature = "html")] -// { -// use ammonia; -// use std::collections::HashSet; -// use std::iter::FromIterator; -// // TODO dont initialize each time -// // TODO sanitize id, classes, etc. -// let tags = HashSet::from_iter(vec!["a", "b", "i", "br", "p", "span", "u"]); -// if core_email.ctype.mimetype == "text/html" { -// let a = ammonia::Builder::new() -// .tags(tags) -// .clean(&core_email.get_body().unwrap_or("".to_string())) -// .to_string(); -// return Ok(a); -// } -// } + #[cfg(feature = "html")] + {} -// if core_email.ctype.mimetype == "text/plain" { -// // TODO html escape this. -// return Ok(core_email.get_body().unwrap_or("".to_string())); -// } -// return Ok(String::from("[No valid body found]")); -// } + if core_email.ctype.mimetype == "text/plain" { + // TODO html escape this. + return Ok(core_email.get_body().unwrap_or("".to_string())); + } + return Ok(String::from("[No valid body found]")); +} diff --git a/crabmail/src/main.rs b/crabmail/src/main.rs @@ -30,11 +30,48 @@ struct Email { in_reply_to: Option<String>, date: u64, // unix epoch. received date body: String, + mime: String, // raw_email: String, } +#[cfg(feature = "html")] +fn parse_html_body(email: &ParsedMail) -> String { + use ammonia; + use std::collections::HashSet; + use std::iter::FromIterator; + // TODO dont initialize each time + // TODO sanitize id, classes, etc. + let tags = HashSet::from_iter(vec!["a", "b", "i", "br", "p", "span", "u"]); + let a = ammonia::Builder::new() + .tags(tags) + .clean(&email.get_body().unwrap_or("".to_string())) + .to_string(); + a +} + fn local_parse_email(data: &[u8]) -> Result<Email> { let parsed_mail = parse_mail(data)?; + let mut body: String = "[Message has no body]".to_owned(); + let mut mime: String = "".to_owned(); + let nobody = "[No body found]"; + if parsed_mail.subparts.len() == 0 { + body = parsed_mail.get_body().unwrap_or(nobody.to_owned()); + } else { + for sub in &parsed_mail.subparts { + if sub.ctype.mimetype == "text/plain" { + body = sub.get_body().unwrap_or(nobody.to_owned()); + mime = sub.ctype.mimetype.clone(); + break; + } + } + #[cfg(feature = "html")] + for sub in &parsed_mail.subparts { + if sub.ctype.mimetype == "text/html" { + mime = sub.ctype.mimetype.clone(); + break; + } + } + } let headers = parsed_mail.headers; let id = headers .get_first_value("message-id") @@ -52,10 +89,10 @@ fn local_parse_email(data: &[u8]) -> Result<Email> { let date = dateparse( &headers .get_first_value("received") - .context("No date header")?, + .unwrap_or(headers.get_first_value("date").context("No date header")?), )? as u64; let from = headers.get_first_value("from").context("No from header")?; - let body = "lorem ipsum".to_owned(); + return Ok(Email { id, in_reply_to, @@ -63,6 +100,7 @@ fn local_parse_email(data: &[u8]) -> Result<Email> { subject, date, body, + mime, }); } @@ -87,7 +125,10 @@ fn main() -> Result<()> { let mut email_index: HashMap<String, Email> = HashMap::new(); for entry in mbox.iter() { let buffer = entry.message().unwrap(); - let email = local_parse_email(buffer)?; + let email = match local_parse_email(buffer) { + Ok(e) => e, + Err(_) => continue, + }; // TODO fix borrow checker if let Some(reply) = email.in_reply_to.clone() { match thread_index.get(&reply) { diff --git a/crabmail/templates/base.html b/crabmail/templates/base.html @@ -3,9 +3,9 @@ <head> <meta charset="utf-8"> <meta http-equiv="Permissions-Policy" content="interest-cohort=()"/> - <link rel="stylesheet" type="text/css" href="style.css" /> + <link rel="stylesheet" type="text/css" href="../style.css" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,user-scalable=0" /> - <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📧</text></svg> + <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📧</text></svg>"> <meta name="description" content="Crabmail mailing list"> <title>Crabmail Mailing List</title> {% block head %}{% endblock %} diff --git a/crabmail/templates/static/style.css b/crabmail/templates/static/style.css @@ -16,8 +16,27 @@ table { border-spacing: 0.5em 0.1em; } font-family: "Roboto Mono", monospace; } + .email-body { white-space:pre-line; + font-family:monospace; + font-size: 1rem; +} + +.button { + background-color: blue; + border: none; + color: white; + text-align: center; + text-decoration: none; + padding: 3px 3px; + display: inline-block; + font-size: 16px; +} + +.button:hover { + background-color: green; + color: white; } .filesize { @@ -41,7 +60,6 @@ h1, h2, h3 { } h3 { - display: inline-block; margin-right: 2em; } diff --git a/crabmail/templates/thread.html b/crabmail/templates/thread.html @@ -3,14 +3,15 @@ {% block content %} <div class="page-title"><h1>{{root.subject}}</h1></div> <div> - <div class="message"> {% for message in messages %} - <h3>{{message.subject}}</h3> - <b>From: </b>{{message.from}}<br> - <b>Date: </b>{{message.date | time_ago}}<br> + <div class="message"> + <h3 id="{{message.id}}"><a href="#{{message.id}}">#</a> {{message.subject}}</h3> + <b>From: </b>{{message.from}} <em>{{message.date | time_ago}}</em><br> <div class="email-body"> {{message.body}} </div> + <a class="button">reply</a> + <hr> {% endfor %} </div> </div>