crabmail

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

commit 335532a2bf61068989d327bff02d6aefad748e81
parent ca0d100118acbee6d9a7040d2fc49613bb0d8f48
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 20 Mar 2022 15:36:22 -0700

WIP xml

Diffstat:
MTODO | 7+++++--
Msrc/main.rs | 21+++++++++++++++++++--
Msrc/models.rs | 14+++++++++++++-
Msrc/templates/gmi.rs | 1+
Msrc/templates/xml.rs | 159+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/threading.rs | 2+-
6 files changed, 109 insertions(+), 95 deletions(-)

diff --git a/TODO b/TODO @@ -2,11 +2,14 @@ TODO ==== atom feeds working -> pull last x into threads paginate list home +only unformat flowed if we are format flowed in gmi, html, xml + +feature water line +---------- +test a ton reference mblaze command, add examples to readme fix docs check for html escape bugz - Duplicate ID verification: warn on duplicate ID, use first received-date. This is to prevent someone overwriting old emails secretly - Color highlight on anchor select diff --git a/src/main.rs b/src/main.rs @@ -3,6 +3,7 @@ // that is ok though #[forbid(unsafe_code)] use anyhow::{Context, Result}; +use mail_parser::Message; use maildir::Maildir; use std::collections::HashSet; use std::fs; @@ -22,6 +23,8 @@ mod util; use std::ffi::{OsStr, OsString}; +const ATOM_ENTRY_LIMIT: usize = 100; + // stole it from the internet pub fn append_ext(ext: impl AsRef<OsStr>, path: &PathBuf) -> PathBuf { let mut os_string: OsString = path.into(); @@ -89,7 +92,21 @@ impl List { write_if_unchanged(&index.with_extension("html"), html.as_bytes()); } } - // write_if_unchanged(&self.out_dir.join("atom.xml"), self.to_xml().as_bytes()); + write_if_unchanged(&self.out_dir.join("atom.xml"), self.to_xml().as_bytes()); + } + + // Used with atom + fn get_recent_messages(&self) -> Vec<StrMessage> { + let mut out = Vec::new(); + let mut msgs: Vec<&threading::Msg> = self.thread_idx.threads.iter().flatten().collect(); + msgs.sort_by_key(|x| x.time); + msgs.reverse(); + for m in msgs.iter().take(ATOM_ENTRY_LIMIT) { + let data = std::fs::read(&m.path).unwrap(); + let msg = StrMessage::new(&Message::parse(&data).unwrap()); + out.push(msg); + } + out } fn write_threads(&mut self) { @@ -99,7 +116,6 @@ impl List { let message_dir = self.out_dir.join("messages"); std::fs::create_dir_all(&thread_dir).ok(); std::fs::create_dir_all(&message_dir).ok(); - // Used for atom for thread_ids in &self.thread_idx.threads { // Load thread let thread = Thread::new(thread_ids, &self.config.name); @@ -132,6 +148,7 @@ impl List { } self.thread_topics.sort_by_key(|t| t.last_reply); self.thread_topics.reverse(); + self.recent_messages = self.get_recent_messages(); // Remove deleted stuff for dir in vec![message_dir, thread_dir] { diff --git a/src/models.rs b/src/models.rs @@ -32,6 +32,7 @@ impl Lists { thread_idx, config, thread_topics: vec![], + recent_messages: vec![], out_dir: self.out_dir.join(name), }) } @@ -39,7 +40,8 @@ impl Lists { pub struct List { pub thread_idx: ThreadIdx, pub thread_topics: Vec<ThreadSummary>, // TODO - pub config: Subsection, // path + pub recent_messages: Vec<StrMessage>, + pub config: Subsection, // path pub out_dir: PathBuf, } @@ -74,6 +76,7 @@ pub struct StrMessage { pub id: String, pub subject: String, pub thread_subject: String, + pub received: i64, pub preview: String, pub from: MailAddress, pub date: String, // TODO better dates @@ -183,6 +186,14 @@ impl StrMessage { pub fn new(msg: &Message) -> StrMessage { let id = msg.get_message_id().unwrap_or(""); + // TODO duplicate in threading.rs + let received = msg + .get_received() + .as_datetime_ref() + .or_else(|| msg.get_date()) + .unwrap() + .to_timestamp() + .unwrap_or(-1); let subject = msg.get_subject().unwrap_or("(No Subject)"); let thread_subject = msg.get_thread_name().unwrap_or("(No Subject)"); let invalid_email = Addr::new(None, "invalid-email"); @@ -236,6 +247,7 @@ impl StrMessage { id: id.to_owned(), subject: subject.to_owned(), from: from, + received, preview, to, cc, diff --git a/src/templates/gmi.rs b/src/templates/gmi.rs @@ -114,6 +114,7 @@ To: {to}{optional_headers} ("mailto", &h(&msg.mailto)), ("msg_path", &h(msg.pathescape_msg_id().to_str().unwrap())), // TODO escape # in body? + // TODO only unformat flowed if flowed is true ("body", &unformat_flowed(&msg.body)), ], ) diff --git a/src/templates/xml.rs b/src/templates/xml.rs @@ -1,100 +1,81 @@ -// use super::util::xml_escape; +use super::util::xml_safe as x; use crate::models::*; +use crate::time::Date; +use crate::util::unformat_flowed; // use crate::templates::util::xml_safe; // use anyhow::{Context, Result}; -// use nanotemplate::template; +use nanotemplate::template; -// const ATOM_ENTRY_LIMIT: i32 = 100; +const FEED_TEMPLATE: &str = r#"<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> +<title>{feed_title}</title> +<link href="{feed_link}"/> +<updated>{last_updated}</updated> +<author> +<name>{author_name}</name> +<email>{author_email}</email> +</author> +<id>{feed_id}</id> +{entry_list} +</feed>"#; -// impl List { -// fn to_xml(&self) { -// template( -// r#"<?xml version="1.0" encoding="utf-8"?> -// <feed xmlns="http://www.w3.org/2005/Atom"> -// <title>{feed_title}</title> -// <link href="{feed_link}"/> -// <updated>{last_updated}</updated> -// <author> -// <name>{author_name}</name> -// <email>{author_email}</email> -// </author> -// <id>{feed_id}</id> -// {entry_list} -// </feed>"#, -// &[], -// // feed_title = &self.name, -// // feed_link = &self.url, -// // last_updated = time::secs_to_date(last_updated).rfc3339(), -// // author_name = &self.email, -// // author_email = &self.email, -// // feed_id = &self.url, -// // entry_list = entries_str, -// ) -// } -// } +const MESSAGE_TEMPLATE: &str = r#"<entry> +<title>{title}</title> +<link href="tbd"/> +<id>{entry_id}</id> +<updated>{updated_at}</updated> +<author> +<name>{author_name}</name> +<email>{author_email}</email> +</author> +<content type="text/plain"> +{content} +</content> +</entry> +"#; + +impl List { + pub fn to_xml(&self) -> String { + let mut entry_list = String::new(); + for msg in &self.recent_messages { + entry_list.push_str( + &template( + MESSAGE_TEMPLATE, + &[ + ("title", &x(&msg.subject)), + // ("item_link", "sdf"), + ("entry_id", &x(&msg.id)), + ("updated_at", &Date::from(msg.received).rfc3339()), + ( + "author_name", + &x(&msg.from.clone().name.unwrap_or(msg.from.clone().address)), + ), + ("author_email", &x(&msg.from.address)), + ("content", &x(&unformat_flowed(&msg.body))), + ], + ) + .unwrap(), + ); + } + template( + FEED_TEMPLATE, + &[ + ("feed_link", "asdf"), + ("feed_id", "asdf"), + ("feed_title", "asdf"), + ("last_updated", "adf"), + ("entry_list", &entry_list), + ("author_name", ""), + ("author_email", ""), + ], + ) + .unwrap() + // last_updated = time::secs_to_date(last_updated).rfc3339(), + } +} impl Thread { pub fn to_xml(&self) -> String { String::new() } } -// return "" -// for message in &self.messages { -// let tmpl = self.build_msg_atom(message); -// entries.push_str(&tmpl); -// } -// let root = self.messages[0]; -// let atom = format!( -// r#"<?xml version="1.0" encoding="utf-8"?> -// <feed xmlns="http://www.w3.org/2005/Atom"> -// <title>{feed_title}</title> -// <link rel="self" href="{feed_link}"/> -// <updated>{last_updated}</updated> -// <author> -// <name>{author_name}</name> -// <email>{author_email}</email> -// </author> -// <id>{feed_id}</id> -// {entry_list} -// </feed>"#, -// feed_title = xml_safe(&root.subject), -// feed_link = self.url(), -// last_updated = time::secs_to_date(self.last_reply()).rfc3339(), -// author_name = xml_safe(short_name(&root.from)), -// author_email = xml_safe(&root.from.addr), -// feed_id = self.url(), -// entry_list = entries, -// ); -// } -// } - -// impl<'a> StrMessage<'a> { -// fn to_xml(&self) -> String { -// template( -// r#"<entry> -// <title>{title}</title> -// <link href="{item_link}"/> -// <id>{entry_id}</id> -// <updated>{updated_at}</updated> -// <author> -// <name>{author_name}</name> -// <email>{author_email}</email> -// </author> -// <content type="text/plain"> -// {content} -// </content> -// </entry> -// "#, -// &[ -// ("title", self.subject.as_ref()), -// // ("item_link", "TBD"), -> this introduces filesystem dependency -// // ("entry_id", self.id), -// ("updated_at", "TBD"), -// // ("author_name", self.from.name), -// // ("author_email", self.from.address), -// // ("content", self.body), -// ], -// ) -// .unwrap() -// } -// } diff --git a/src/threading.rs b/src/threading.rs @@ -4,7 +4,7 @@ // stores use mail_parser::parsers::fields::thread::thread_name; -use mail_parser::{Message}; +use mail_parser::Message; use std::collections::HashMap; use std::path::PathBuf;