crabmail

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

commit d857ddf4dadb2fd313d0da2bbadb5863e17cc58c
parent 4e54e7945985f51f236ddc5e474e4410ee71b23f
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sat, 19 Mar 2022 22:03:16 -0700

Fix gemini output

Diffstat:
MTODO | 7++-----
Msrc/models.rs | 14++++++++++++++
Msrc/templates/gmi.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/templates/html.rs | 51---------------------------------------------------
Msrc/util.rs | 29+++++++++++++++++++++++++++++
5 files changed, 110 insertions(+), 62 deletions(-)

diff --git a/TODO b/TODO @@ -1,18 +1,15 @@ TODO ==== -understand content-transfer-encoding for eml body -use get_thread_name? -get export working -> use mail-builder (fork it and/or cut deps) -get reply link working allow mbox/single folder input atom feeds working gemini pages finish up +-> gemini body parser paginate list home create list home page - dkim? + Duplicate ID verification: warn on duplicate ID, use first received-date. This is to prevent someone overwriting old emails secretly diff --git a/src/models.rs b/src/models.rs @@ -120,6 +120,20 @@ impl MailAddress { address: address.unwrap().to_string(), } } + + pub fn to_string(&self) -> String { + let mut out = String::new(); + if let Some(n) = &self.name { + out.push('"'); + out.push_str(&n); + out.push('"'); + out.push(' '); + } + out.push('<'); + out.push_str(&self.address); + out.push('>'); + out + } } // TODO rename diff --git a/src/templates/gmi.rs b/src/templates/gmi.rs @@ -1,6 +1,8 @@ // WIP // use crate::models::*; +use crate::time::Date; +use crate::util::*; use nanotemplate::template; impl Lists { @@ -17,7 +19,41 @@ impl Lists { impl List { pub fn to_gmi(&self) -> Vec<String> { - vec![] + // TODO paginate + let mut threads = "# list name".to_string(); + for thread in &self.thread_topics { + threads.push_str( + // TODO reuse with html templates? + &template( + r#" +=> threads/{path_id}.gmi {subject} +{preview} +{from} | {replies} replies | {date} +"#, + &[ + ( + "path_id", + &h(thread.message.pathescape_msg_id().to_str().unwrap()), + ), + ("subject", &h(&thread.message.subject)), + ("replies", &thread.reply_count.to_string()), + ("preview", &h(&thread.message.preview)), + ("date", &h(&Date::from(thread.last_reply).ymd())), + ( + "from", + &h(&thread. // awkawrd + message.from + .name + .clone() + .unwrap_or(thread.message.from.address.clone()) + .clone()), + ), + ], + ) + .unwrap(), + ); + } + vec![threads] } } @@ -29,24 +65,47 @@ impl Thread { self.messages[0].subject.replace("\n", " ") ); for msg in &self.messages { + let mut optional_headers = String::new(); + if let Some(irt) = &msg.in_reply_to { + optional_headers.push_str(&format!("\nIn-Reply-To: {}", &h(&irt))); + } + // TODO no copy pasta + let cc_string = &h(&msg + .cc + .iter() + .map(|t| t.to_string()) + .collect::<Vec<String>>() + .join(", ")); + if msg.cc.len() > 0 { + optional_headers.push_str(&format!("\nCc: {}", cc_string)); + } let msg = template( r#" ## {subject} From: {from} Date: {date} -In-Reply-To: adsf Message-Id: {msg_id} -To: ... -Cc: ... ---------------------- +To: {to}{optional_headers} +-------------------------------------- {body} "#, &[ ("subject", &h(&msg.subject)), ("date", &h(&msg.date)), ("msg_id", &h(&msg.id)), + ( + "to", + &h(&msg + .to + .iter() + .map(|t| t.to_string()) + .collect::<Vec<String>>() + .join(", ")), + ), + ("optional_headers", &optional_headers), ("from", &h(&msg.from.address)), - ("body", &escape_body(&msg.body)), + // TODO escape # in body? + ("body", &unformat_flowed(&msg.body)), ], ) .unwrap(); diff --git a/src/templates/html.rs b/src/templates/html.rs @@ -228,28 +228,6 @@ impl Thread { } } -impl StrMessage { - pub fn to_html(&self) -> String { - // TODO test thoroughly - template( - r#"<div id="{id}", class="message"> - <div class="message-meta"> - <span class="bold"> - {subject} - </span> - <a href="mailto:{from}" class="bold">{from}</a> - <span>{date}</span> - <a class="permalink" href=#{id}>🔗</a> - </div> - </div> - etc - "#, - &[("id", "asdf")], - ) - .unwrap() - } -} - // gpl licensed from wikipedia https://commons.wikimedia.org/wiki/File:Generic_Feed-icon.svg pub const RSS_SVG: &'static str = r#" data:image/svg+xml,<?xml version="1.0" encoding="UTF-8"?> @@ -320,32 +298,3 @@ pub fn email_body(body: &str, flowed: bool) -> String { // TODO err conversion String::from_utf8(bytes).unwrap() } - -// TODO MOVE! -// stolen from https://github.com/deltachat/deltachat-core-rust/blob/master/src/format_flowed.rs -// undoes format=flowed -pub fn unformat_flowed(text: &str) -> String { - let mut result = String::new(); - let mut skip_newline = true; - - for line in text.split('\n') { - // Revert space-stuffing - let line = line.strip_prefix(' ').unwrap_or(line); - - if !skip_newline { - result.push('\n'); - } - - if let Some(line) = line.strip_suffix(' ') { - // Flowed line - result += line; - result.push(' '); - skip_newline = true; - } else { - // Fixed line - result += line; - skip_newline = false; - } - } - result -} diff --git a/src/util.rs b/src/util.rs @@ -12,3 +12,32 @@ pub fn truncate_ellipsis(s: &str, n: usize) -> String { out.push_str("..."); out.to_string() } + +// TODO quoted lines? +// stolen from https://github.com/deltachat/deltachat-core-rust/blob/master/src/format_flowed.rs +// undoes format=flowed +pub fn unformat_flowed(text: &str) -> String { + let mut result = String::new(); + let mut skip_newline = true; + + for line in text.split('\n') { + // Revert space-stuffing + let line = line.strip_prefix(' ').unwrap_or(line); + + if !skip_newline { + result.push('\n'); + } + + if let Some(line) = line.strip_suffix(' ') { + // Flowed line + result += line; + result.push(' '); + skip_newline = true; + } else { + // Fixed line + result += line; + skip_newline = false; + } + } + result +}