crabmail

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

commit dc47475c4de3c82970a8f915f1889c065a9c00e4
parent b47633660a6c8f2682062b2b9e97894410cebec7
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Thu, 17 Mar 2022 21:32:22 -0700

added some html stufz

Diffstat:
MCargo.lock | 16++++++++--------
Msrc/main.rs | 16++++++++++------
Msrc/models.rs | 20+++++++++++++++++++-
Msrc/templates/html.rs | 46+++++++++++++++++++++++++++++++++++-----------
4 files changed, 72 insertions(+), 26 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "anyhow" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" [[package]] name = "cfg-if" @@ -47,9 +47,9 @@ dependencies = [ [[package]] name = "mail-parser" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f31ffdaf350a570dac60afd70433dca8b2a4027f98064a8fca68549a54d2326" +checksum = "545e2643e5f923cdea238bb826277e68967797f7d50523932d10a87dfcc940ac" dependencies = [ "encoding_rs", "serde", @@ -75,9 +75,9 @@ checksum = "fcde141f0a9acabd38860369eeb0d69f1756d19c5948672c211e82e0519edd61" [[package]] name = "once_cell" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "proc-macro2" @@ -119,9 +119,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "ebd69e719f31e88618baa1eaa6ee2de5c9a1c004f1e9ecdb58e8352a13f20a01" dependencies = [ "proc-macro2", "quote", diff --git a/src/main.rs b/src/main.rs @@ -26,7 +26,7 @@ const PAGE_SIZE: i32 = 100; // TODO impl Lists { - fn write_lists(&self) { + fn write_lists(&mut self) { std::fs::create_dir_all(&self.out_dir); let css = include_bytes!("style.css"); write_if_unchanged(&self.out_dir.join("style.css"), css); @@ -35,8 +35,10 @@ impl Lists { if Config::global().include_gemini { write_if_unchanged(&base_path.with_extension("gmi"), self.to_gmi().as_bytes()); } - for list in &self.lists { - list.persist() + for list in &mut self.lists { + list.persist(); + // todo somewhat awkward + write_if_unchanged(&list.out_dir.join("style.css"), css); } } } @@ -63,7 +65,7 @@ enum Format { } impl List { - fn persist(&self) { + fn persist(&mut self) { // let written = hashset self.write_index(); self.write_threads(); @@ -95,11 +97,10 @@ impl List { // write_if_unchanged(&self.out_dir.join("atom.xml"), self.to_xml().as_bytes()); } - fn write_threads(&self) { + fn write_threads(&mut self) { // files written = HashSet let thread_dir = self.out_dir.join("threads"); std::fs::create_dir_all(&thread_dir).unwrap(); - self.write_index(); for thread_ids in &self.thread_idx.threads { // Load thread let thread = Thread::new(thread_ids); @@ -112,9 +113,12 @@ impl List { if Config::global().include_gemini { write_if_unchanged(&basepath.with_extension("gmi"), thread.to_gmi().as_bytes()); } + // this is a bit awkward + self.thread_topics.push(thread.messages[0].clone()); // for file in thread // write raw file } + self.write_index(); } } diff --git a/src/models.rs b/src/models.rs @@ -13,6 +13,8 @@ use std::path::PathBuf; // raw_email = "/{list_name}/messages/{message_id}.eml // paginate index somehow (TBD) +// TODO a better way to handle these is to use lifetimes rather than ownership +// I should implement an iterator that writes each message without holding them in memory probably pub struct Lists { pub lists: Vec<List>, pub out_dir: PathBuf, @@ -28,13 +30,14 @@ impl Lists { self.lists.push(List { thread_idx, config, + thread_topics: vec![], out_dir: self.out_dir.join(name), }) } } pub struct List { pub thread_idx: ThreadIdx, - // Thread topics + pub thread_topics: Vec<StrMessage>, pub config: Subsection, // path pub out_dir: PathBuf, } @@ -49,6 +52,7 @@ impl List { Self { thread_idx: ThreadIdx::default(), config: sub, + thread_topics: vec![], out_dir: Config::global().out_dir.join(name), } } @@ -72,9 +76,11 @@ impl Thread { // simplified, stringified-email for templating // making everything owned because I'm l a z y +#[derive(Debug, Clone)] pub struct StrMessage { pub id: String, pub subject: String, + pub preview: String, pub from: MailAddress, pub date: String, // TODO better dates pub body: String, @@ -97,6 +103,7 @@ impl StrMessage { } // i suck at Cow and strings +#[derive(Debug, Clone)] pub struct MailAddress { pub name: Option<String>, pub address: String, @@ -118,6 +125,10 @@ impl StrMessage { let id = msg.get_message_id().unwrap_or(""); let subject = msg.get_subject().unwrap_or("(No Subject)"); let invalid_email = Addr::new(None, "invalid-email"); + let preview = match msg.get_body_preview(100) { + Some(b) => b.to_string(), + None => String::new(), + }; let from = match msg.get_from() { HeaderValue::Address(fr) => fr, _ => &invalid_email, @@ -143,6 +154,7 @@ impl StrMessage { .map(|x| x.c_type.to_string()) .unwrap_or(String::new()), from: from, + preview, to: vec![], cc: vec![], date: date.to_owned(), @@ -151,3 +163,9 @@ impl StrMessage { } } } + +// Export the email, not as it originally is, but a "clean" version of it +// Maybe based off of https://git.causal.agency/bubger/tree/export.c +fn raw_export(msg: &Message) -> String { + String::new() +} diff --git a/src/templates/html.rs b/src/templates/html.rs @@ -10,10 +10,11 @@ const header: &str = r#"<!DOCTYPE html> <head> <title>{title}</title> <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>'></head> <meta name="description" content="{title}"/> +</head> <body> "#; @@ -26,12 +27,11 @@ Archive generated with <a href='https://crabmail.flounder.online/'>crabmail</a> impl Lists { pub fn to_html(&self) -> String { + let body = r#"<h1 class="page-title">{title}</h1> + <a href="atom.xml"><img alt="Atom Feed" src='{rss_svg}' /></a>"#; template( - r#" - <h1>Mail Archives</h1> - <hr> - "#, - &[("title", "tbd")], + &format!("{}{}{}", header, body, footer), + &[("rss_svg", RSS_SVG), ("title", "tbd")], ) .unwrap() } @@ -40,25 +40,49 @@ impl Lists { impl List { pub fn to_html(&self) -> Vec<String> { // TODO paginate - let threads = String::new(); + let mut threads = String::new(); + for thread in &self.thread_topics { + threads.push_str( + &template( + r#" + <div class='message-sum'> + <a class="bigger" href="threads/{path_id}.html">{subject}</a> + <br> + {preview}<br> + joe@schmoe.com | n replies | last-reply-date + "#, + &[ + ("path_id", &x(thread.pathescape_msg_id().to_str().unwrap())), + ("subject", &thread.subject), + ("date", &thread.date), + ("preview", &thread.preview), + ], + ) + .unwrap(), + ); + } // TODO use summary?? let page = template( r#" {header} <h1 class="page-title"> {title} - </h1> <a href="atom.xml"> - <img alt="Atom feed" src={rss_svg} /> - {threads} + <img alt="Atom feed" src='{rss_svg}' /> </a> </h1> + {description}<br> + <a href="{mailto:list_email}">{list_email}</a> + <hr> + {threads} {footer} "#, &[ ("header", header), + ("description", &self.config.description), ("title", self.config.title.as_str()), ("threads", &threads), + ("list_email", &self.config.email), ("rss_svg", RSS_SVG), ("footer", footer), ], @@ -157,7 +181,7 @@ impl Thread { .unwrap(), ); } - out.push_str("</div><hr>"); + out.push_str("</div><hr></body></html>"); // body out }