crabmail

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

commit 698e8ccf7ae68326ee45c0cc00c3725fb28ca30b
parent bb02f1103cf193bcb8558f1b28569b935787cbca
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 13 Mar 2022 13:41:31 -0700

WIP...

Diffstat:
Msrc/main.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/models.rs | 56+++++++++++++++++++++++++++++---------------------------
Msrc/templates/gmi.rs | 10++++++++--
Msrc/templates/html.rs | 13+++++++------
Msrc/templates/xml.rs | 2+-
Msrc/threading.rs | 3++-
6 files changed, 104 insertions(+), 61 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -25,17 +25,7 @@ const PAGE_SIZE: i32 = 100; // TODO -impl Lists<'_> { - fn add(&mut self, data: threading::ThreadIdx, name: &str) { - let newlist = List::new(name); - for thread in data.threads { - // let ids = thread.iter().map(|m| m.id).collect(); - // newlist.threads.push(Thread::from_id_list(ids)); - } - // sort threads - self.lists.push(newlist); - } - +impl Lists { fn write_lists(&self) { std::fs::create_dir_all(&self.out_dir); let css = include_bytes!("style.css"); @@ -46,7 +36,7 @@ impl Lists<'_> { write_if_unchanged(&base_path.with_extension("gmi"), self.to_gmi().as_bytes()); } for list in &self.lists { - list.write_all_files() + list.persist() } } } @@ -75,23 +65,54 @@ enum Format { GMI, } -impl List<'_> { +impl List { // TODO move to main // fn from_maildir() -> Self { // TODO figure out init // where to live // List { threads: vec![] } - fn write_all_files(&self) { - let index = self.out_dir.join("index"); + // + + fn persist(&self) { + // let written = hashset + self.write_index(); + self.write_threads(); + // for file in threads, messages + // if not in written + // delete + } + fn write_index(&self) { + // TODO fix lazy copy paste + // TODO return files written + for (n, gmi) in self.to_gmi().iter().enumerate() { + let index; + if n == 0 { + index = self.out_dir.join("index"); + } else { + index = self.out_dir.join(format!("{}-{}", "index", n)); + } + write_if_unchanged(&index.with_extension("gmi"), gmi.as_bytes()); + } + for (n, html) in self.to_html().iter().enumerate() { + let index; + if n == 0 { + index = self.out_dir.join("index"); + } else { + index = self.out_dir.join(format!("{}-{}", "index", n)); + } + write_if_unchanged(&index.with_extension("html"), html.as_bytes()); + } + // write_if_unchanged(&self.out_dir.join("atom.xml"), self.to_xml().as_bytes()); + } + + fn write_threads(&self) { + // files written = HashSet let thread_dir = self.out_dir.join("threads"); std::fs::create_dir_all(&thread_dir).unwrap(); - write_if_unchanged(&self.out_dir.join("atom.xml"), self.to_html().as_bytes()); - - // TODO write index (paginated) gmi - // write index (paginated) html - // Delete threads that aren't in my list (xml, gmi, html) - for thread in &self.threads { + self.write_index(); + for thread_ids in &self.thread_idx.threads { + // Load thread + let thread = Thread::new(thread_ids); let basepath = thread_dir.join(&pathescape_msg_id(&thread.messages[0].id)); - // TODO cleanup, abstract write_if_unchanged( &basepath.with_extension("html"), thread.to_html().as_bytes(), @@ -100,7 +121,6 @@ impl List<'_> { if Config::global().include_gemini { write_if_unchanged(&basepath.with_extension("gmi"), thread.to_gmi().as_bytes()); } - // Delete nonexistent messages (cache?) // for file in thread // write raw file } @@ -140,12 +160,25 @@ fn main() -> Result<()> { } // id list into thread list.finalize(); - lists.add(list, &dir_name); + // lists.lists.push(list); } lists.write_lists(); Ok(()) } + +// TODO del this stuff +// +// +// +// +// +// +// +// +// +// +// // impl<'a> MailThread<'a> {} // pub fn write_to_file(&self) -> Result<()> { diff --git a/src/models.rs b/src/models.rs @@ -1,4 +1,5 @@ use crate::config::{Config, Subsection}; +use crate::threading::{Msg, ThreadIdx}; use mail_parser::{Addr, HeaderValue, Message}; use std::borrow::Cow; use std::path::PathBuf; @@ -11,18 +12,19 @@ use std::path::PathBuf; // raw_email = "/{list_name}/messages/{message_id}.eml // paginate index somehow (TBD) -pub struct Lists<'a> { - pub lists: Vec<List<'a>>, +pub struct Lists { + pub lists: Vec<List>, pub out_dir: PathBuf, } -pub struct List<'a> { - pub threads: Vec<Thread<'a>>, +pub struct List { + pub thread_idx: crate::threading::ThreadIdx, + // Thread topics pub config: Subsection, // path pub out_dir: PathBuf, } -impl List<'_> { +impl List { pub fn new(name: &str) -> Self { let con = Config::global(); let sub: Subsection = match con.get_subsection(name) { @@ -30,37 +32,37 @@ impl List<'_> { None => con.default_subsection(name), }; Self { - threads: vec![], + thread_idx: crate::threading::ThreadIdx::default(), config: sub, out_dir: Config::global().out_dir.join(name), } } } -pub struct Thread<'a> { - pub messages: Vec<StrMessage<'a>>, +pub struct Thread { + pub messages: Vec<StrMessage>, } -impl Thread<'_> { - // fn new() -> Self { - // Thread {messagse: } - // } +impl Thread { + pub fn new(thread_idx: &Vec<Msg>) -> Self { + Thread { messages: vec![] } + } } -// TODO rename // simplified, stringified-email for templating -pub struct StrMessage<'a> { - pub id: Cow<'a, str>, - pub subject: Cow<'a, str>, +// making everything owned because I'm l a z y +pub struct StrMessage { + pub id: String, + pub subject: String, pub from: MailAddress, - pub date: Cow<'a, str>, // TODO better dates - pub body: Cow<'a, str>, - pub in_reply_to: Option<Cow<'a, str>>, + pub date: String, // TODO better dates + pub body: String, + pub in_reply_to: Option<String>, // url: Cow<'a, str>, // download_path: PathBuf, // TODO } -impl StrMessage<'_> { +impl StrMessage { pub fn from_file() {} } @@ -89,8 +91,8 @@ impl MailAddress { } // TODO rename -impl<'a> Thread<'a> { - fn new_message(msg: &'a Message<'a>) -> StrMessage<'a> { +impl StrMessage { + pub fn new(msg: &Message) -> 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"); @@ -103,7 +105,7 @@ impl<'a> Thread<'a> { let in_reply_to = msg .get_in_reply_to() .as_text_ref() - .and_then(|a| Some(Cow::Borrowed(a))); + .and_then(|a| Some(a.to_string())); // TODO linkify body // TODO unformat-flowed @@ -111,11 +113,11 @@ impl<'a> Thread<'a> { .get_text_body(0) .unwrap_or(Cow::Borrowed("[No message body]")); StrMessage { - id: Cow::Borrowed(id), - subject: Cow::Borrowed(subject), + id: id.to_owned(), + subject: subject.to_owned(), from: from, - date: Cow::Owned(date), - body: body, + date: date.to_owned(), + body: body.to_string(), in_reply_to: in_reply_to, } } diff --git a/src/templates/gmi.rs b/src/templates/gmi.rs @@ -1,7 +1,7 @@ use crate::models::*; use nanotemplate::template; -impl Lists<'_> { +impl Lists { pub fn to_gmi(&self) -> String { template( r#" @@ -13,7 +13,13 @@ impl Lists<'_> { } } -impl Thread<'_> { +impl List { + pub fn to_gmi(&self) -> Vec<String> { + vec![] + } +} + +impl Thread { pub fn to_gmi(&self) -> String { String::new() } diff --git a/src/templates/html.rs b/src/templates/html.rs @@ -21,7 +21,7 @@ const layout: &str = r#"<!DOCTYPE html> </html> "#; -impl Lists<'_> { +impl Lists { pub fn to_html(&self) -> String { template( r#" @@ -34,8 +34,8 @@ impl Lists<'_> { } } -impl List<'_> { - pub fn to_html(&self) -> String { +impl List { + pub fn to_html(&self) -> Vec<String> { template( r#" <h1 class="page-title"> @@ -47,17 +47,18 @@ impl List<'_> { "#, &[("title", self.config.title.as_str()), ("rss_svg", RSS_SVG)], ) - .unwrap() + .unwrap(); + vec![] } } -impl Thread<'_> { +impl Thread { pub fn to_html(&self) -> String { template(r#""#, &[("title", "tbd")]).unwrap() } } -impl<'a> StrMessage<'a> { +impl StrMessage { pub fn to_html(&self) -> String { // TODO test thoroughly template( diff --git a/src/templates/xml.rs b/src/templates/xml.rs @@ -31,7 +31,7 @@ use nanotemplate::template; // } // } -impl Thread<'_> { +impl Thread { pub fn to_xml(&self) -> String { String::new() } diff --git a/src/threading.rs b/src/threading.rs @@ -1,6 +1,7 @@ // Simple threading algorithm based on https://datatracker.ietf.org/doc/html/rfc8621 // A thread is a collection of messages sorted by date. -// Assumes msg can be found on disk at `path` -- could be made more abstract +// Assumes msg can be found on disk at `path` -- should be made more abstract to handle other mail +// stores use mail_parser::parsers::fields::thread::thread_name; use mail_parser::{DateTime, Message};