crabmail

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

commit 0e2595d26a0dc686b6eeea1c3a718393e163f53b
parent d358780018a0e9956b269e1885d9b2b56687407a
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Fri, 31 Dec 2021 15:17:59 -0800

WIP

Diffstat:
MCargo.lock | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 6++++++
Msrc/arg.rs | 12++++++------
Msrc/main.rs | 282+++++++++++++++++++++++++++++++++++++++++++------------------------------------
4 files changed, 220 insertions(+), 136 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -46,7 +46,9 @@ dependencies = [ "anyhow", "horrorshow", "linkify", + "maildir", "mailparse", + "nanotemplate", "once_cell", "sha3", "urlencoding", @@ -92,6 +94,16 @@ dependencies = [ ] [[package]] +name = "gethostname" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e692e296bfac1d2533ef168d0b60ff5897b8b70a4009276834014dd8924cc028" +dependencies = [ + "libc", + "winapi", +] + +[[package]] name = "horrorshow" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -104,6 +116,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" [[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] name = "linkify" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -113,6 +131,16 @@ dependencies = [ ] [[package]] +name = "maildir" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6adcf4693a3c725b9c69ccf24f8ed9c6d3e7168c1a45632570d65529adc13b5e" +dependencies = [ + "gethostname", + "mailparse", +] + +[[package]] name = "mailparse" version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -130,6 +158,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] +name = "nanotemplate" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcde141f0a9acabd38860369eeb0d69f1756d19c5948672c211e82e0519edd61" + +[[package]] name = "once_cell" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -168,3 +202,25 @@ name = "version_check" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml @@ -7,11 +7,17 @@ edition = "2018" [features] default = [] + +[lib] +proc-macro = true + [dependencies] anyhow = "1.0" horrorshow = "0.8" linkify = "0.8" mailparse = "0.13" +maildir = "0.5.0" once_cell = "1.9" sha3 = "0.10" urlencoding = "2.1" +nanotemplate = "0.2" diff --git a/src/arg.rs b/src/arg.rs @@ -18,8 +18,8 @@ use std::process::exit; fn usage() -> ! { let name = env::args().next().unwrap(); eprintln!( - "usage: {} [mbox-file] -FLAGS: + "usage: {} [maildir] +FLAGS -r use relative timestamps ARGS: @@ -31,7 +31,7 @@ ARGS: } pub struct Args { - pub mbox: PathBuf, + pub maildir: String, pub config: PathBuf, pub out_dir: PathBuf, pub flags: String, @@ -41,7 +41,7 @@ impl Args { pub fn from_env() -> Self { // Modify as needed let mut flags = String::new(); - let mut mbox: Option<String> = None; + let mut maildir: Option<String> = None; let mut out_dir = "site".into(); let mut config = "crabmail.conf".into(); @@ -55,7 +55,7 @@ impl Args { while let Some(arg) = args.next() { let mut chars = arg.chars(); if chars.next() != Some('-') { - mbox = Some(arg); + maildir = Some(arg); continue; } chars.for_each(|m| match m { @@ -69,7 +69,7 @@ impl Args { } Self { config, - mbox: match mbox { + maildir: match maildir { Some(m) => m.into(), None => usage(), }, diff --git a/src/main.rs b/src/main.rs @@ -3,6 +3,8 @@ use horrorshow::helper::doctype; use horrorshow::owned_html; use horrorshow::prelude::*; use horrorshow::Template; +use maildir::Maildir; +use nanotemplate::template; // TODO write htmlescaper use std::io::BufWriter; use std::path::Path; use std::str; @@ -25,6 +27,7 @@ use utils::xml_safe; mod arg; mod config; mod mbox; +mod threading; mod time; mod utils; @@ -64,6 +67,7 @@ struct MailThread<'a> { } impl<'a> MailThread<'a> {} + fn layout(page_title: impl Render, content: impl Render) -> impl Render { // owned_html _moves_ the arguments into the template. Useful for returning // owned (movable) templates. @@ -471,6 +475,19 @@ fn local_parse_email(data: &[u8]) -> Result<Email> { }); } +// if [arg] has cur,new,tmp -> that is the index +// else, do each subfolder + +// Used when we have more than 1 maildir folder +struct Index {} + +impl Index { + fn write_file() -> Result<()> { + template("Hello, my name is {name}!", &[("name", "nanotemplate")]); + Ok(()) + } +} + fn main() -> Result<()> { let args = arg::Args::from_env(); @@ -480,137 +497,142 @@ fn main() -> Result<()> { INSTANCE.set(config).unwrap(); let out_dir = &Config::global().out_dir; - let mbox = mbox::from_file(&args.mbox)?; - - let mut thread_index: HashMap<String, Vec<String>> = HashMap::new(); - - // index email ID -> email - let mut email_index: HashMap<String, Email> = HashMap::new(); - for entry in mbox { - let buffer = entry.unwrap(); - let email = match local_parse_email(&buffer) { - Ok(e) => e, - Err(e) => { - println!("{:?}", e); - continue; - } - }; - // TODO fix borrow checker - if let Some(reply) = email.in_reply_to.clone() { - match thread_index.get(&reply) { - Some(_) => { - let d = thread_index.get_mut(&reply).unwrap(); - d.push(email.id.clone()); - } - None => { - thread_index.insert(reply, vec![email.id.clone()]); - } - } - } - email_index.insert(email.id.clone(), email); - } - - // Add index by subject lines - // atrocious - let mut todo = vec![]; // im bad at borrow checker - for (_, em) in &email_index { - if em.in_reply_to.is_none() - && (em.subject.starts_with("Re: ") || em.subject.starts_with("RE: ")) - { - // TODO O(n^2) - for (_, em2) in &email_index { - if em2.subject == em.subject[4..] { - match thread_index.get(&em2.id) { - Some(_) => { - let d = thread_index.get_mut(&em2.id).unwrap(); - d.push(em.id.clone()); - } - None => { - thread_index.insert(em2.id.clone(), vec![em.id.clone()]); - } - } - todo.push((em.id.clone(), em2.id.clone())); - break; - } - } - } - } - for (id, reply) in todo { - let em = email_index.get_mut(&id).unwrap(); - em.in_reply_to = Some(reply) - } - - let mut thread_roots: Vec<Email> = email_index - .iter() - .filter_map(|(_, v)| { - if v.in_reply_to.is_none() { - // or can't find root based on Re: subject - return Some(v.clone()); - } - return None; - }) - .collect(); - std::fs::create_dir(&out_dir).ok(); - let thread_dir = Config::global().out_dir.join("threads"); - std::fs::create_dir(&thread_dir).ok(); - let mut threads = vec![]; - let mut curr_threads = get_current_threads(&out_dir); - - for root in &mut thread_roots { - let mut thread_ids = vec![]; - let mut current: Vec<String> = vec![root.id.clone()]; - while current.len() > 0 { - let top = current.pop().unwrap().clone(); - thread_ids.push(top.clone()); - if let Some(ids) = thread_index.get(&top.clone()) { - for item in ids { - current.push(item.to_string()); - } - } - } - - let mut messages: Vec<&Email> = thread_ids - .iter() - .map(|id| email_index.get(id).unwrap()) - .collect(); - - messages.sort_by_key(|a| a.date); - - let mut thread = MailThread { - messages: messages, - hash: root.hash(), - last_reply: 0, // TODO - }; - - thread.last_reply = thread.last_reply(); - - thread.write_to_file()?; - thread.write_atom_feed()?; - curr_threads.remove(&thread.hash); - threads.push(thread); + let maildir = Maildir::from(args.maildir.as_str()); + let mut threader = threading::Arena::default(); + // Loads whole file into memory for threading + for item in maildir.list_cur().chain(maildir.list_new()) { + threader.add_message(item?); } - - for leftover in curr_threads { - let file_to_remove = out_dir.join("threads").join(format!("{}.html", leftover)); - std::fs::remove_file(&file_to_remove).ok(); - let file_to_remove = out_dir.join("threads").join(format!("{}.xml", leftover)); - std::fs::remove_file(&file_to_remove).ok(); - } - - // Remove any threads left over - - threads.sort_by_key(|a| a.last_reply); - threads.reverse(); - let list = ThreadList { threads }; - list.write_to_file()?; - list.write_atom_feed()?; - // kinda clunky - let css = include_bytes!("style.css"); - let mut css_root = File::create(out_dir.join("style.css"))?; - css_root.write(css)?; - let mut css_sub = File::create(out_dir.join("threads").join("style.css"))?; - css_sub.write(css)?; - Ok(()) + threader.finalize(); + return Ok(()); + + // // index email ID -> email + // let mut email_index: HashMap<String, Email> = HashMap::new(); + // for entry in mbox { + // let buffer = entry.unwrap(); + // let email = match local_parse_email(&buffer) { + // Ok(e) => e, + // Err(e) => { + // println!("{:?}", e); + // continue; + // } + // }; + // // TODO fix borrow checker + // if let Some(reply) = email.in_reply_to.clone() { + // match thread_index.get(&reply) { + // Some(_) => { + // let d = thread_index.get_mut(&reply).unwrap(); + // d.push(email.id.clone()); + // } + // None => { + // thread_index.insert(reply, vec![email.id.clone()]); + // } + // } + // } + // email_index.insert(email.id.clone(), email); + // } + + // // Add index by subject lines + // // atrocious + // let mut todo = vec![]; // im bad at borrow checker + // for (_, em) in &email_index { + // if em.in_reply_to.is_none() + // && (em.subject.starts_with("Re: ") || em.subject.starts_with("RE: ")) + // { + // // TODO O(n^2) + // for (_, em2) in &email_index { + // if em2.subject == em.subject[4..] { + // match thread_index.get(&em2.id) { + // Some(_) => { + // let d = thread_index.get_mut(&em2.id).unwrap(); + // d.push(em.id.clone()); + // } + // None => { + // thread_index.insert(em2.id.clone(), vec![em.id.clone()]); + // } + // } + // todo.push((em.id.clone(), em2.id.clone())); + // break; + // } + // } + // } + // } + // for (id, reply) in todo { + // let em = email_index.get_mut(&id).unwrap(); + // em.in_reply_to = Some(reply) + // } + + // let mut thread_roots: Vec<Email> = email_index + // .iter() + // .filter_map(|(_, v)| { + // if v.in_reply_to.is_none() { + // // or can't find root based on Re: subject + // return Some(v.clone()); + // } + // return None; + // }) + // .collect(); + // std::fs::create_dir(&out_dir).ok(); + // let thread_dir = Config::global().out_dir.join("threads"); + // std::fs::create_dir(&thread_dir).ok(); + // let mut threads = vec![]; + // let mut curr_threads = get_current_threads(&out_dir); + + // for root in &mut thread_roots { + // let mut thread_ids = vec![]; + // let mut current: Vec<String> = vec![root.id.clone()]; + // while current.len() > 0 { + // let top = current.pop().unwrap().clone(); + // thread_ids.push(top.clone()); + // if let Some(ids) = thread_index.get(&top.clone()) { + // for item in ids { + // current.push(item.to_string()); + // } + // } + // } + + // let mut messages: Vec<&Email> = thread_ids + // .iter() + // .map(|id| email_index.get(id).unwrap()) + // .collect(); + + // messages.sort_by_key(|a| a.date); + + // let mut thread = MailThread { + // messages: messages, + // hash: root.hash(), + // last_reply: 0, // TODO + // }; + + // thread.last_reply = thread.last_reply(); + + // thread.write_to_file()?; + // thread.write_atom_feed()?; + // curr_threads.remove(&thread.hash); + // threads.push(thread); + // } + + // for leftover in curr_threads { + // let file_to_remove = out_dir.join("threads").join(format!("{}.html", leftover)); + // std::fs::remove_file(&file_to_remove).ok(); + // let file_to_remove = out_dir.join("threads").join(format!("{}.xml", leftover)); + // std::fs::remove_file(&file_to_remove).ok(); + // } + + // // Remove any threads left over + + // threads.sort_by_key(|a| a.last_reply); + // threads.reverse(); + // let list = ThreadList { threads }; + // list.write_to_file()?; + // list.write_atom_feed()?; + // // kinda clunky + // let css = include_bytes!("style.css"); + // let mut css_root = File::create(out_dir.join("style.css"))?; + // css_root.write(css)?; + // let mut css_sub = File::create(out_dir.join("threads").join("style.css"))?; + // css_sub.write(css)?; + // Ok(()) } // Use the sha3 hash of the ID. It is what it is.