crabmail

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

commit 346bc24b2837bb70676c79711316d9b987743cb6
parent c37140f1e7a410769daee3ff47aeb6f1ce2de79b
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sat, 19 Mar 2022 20:36:44 -0700

I think get format/unformat flowed working properly

Diffstat:
Msrc/main.rs | 288-------------------------------------------------------------------------------
Msrc/models.rs | 3++-
Msrc/templates/html.rs | 10++++++++--
3 files changed, 10 insertions(+), 291 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -182,109 +182,6 @@ fn main() -> Result<()> { // TODO del this stuff // // -// -// -// -// -// -// -// -// -// // TODO rename -// pub fn hash(&self) -> String { -// self.id.replace("/", ";") -// } -// } - -// fn local_parse_email(parsed_mail: &ParsedMail) -> Result<Email> { -// let mut body: String = "[Message has no body]".to_owned(); -// let mut mime: String = "".to_owned(); -// let nobody = "[No body found]"; -// // nested lookup -// let mut queue = vec![parsed_mail]; -// let mut text_body = None; -// let mut html_body = None; -// while queue.len() > 0 { -// let top = queue.pop().unwrap(); -// for sub in &top.subparts { -// queue.push(sub); -// } -// let content_disposition = top.get_content_disposition(); -// if content_disposition.disposition == mailparse::DispositionType::Attachment { -// // attachment handler -// } else { -// if top.ctype.mimetype == "text/plain" { -// let b = top.get_body().unwrap_or(nobody.to_owned()); -// if parsed_mail.ctype.params.get("format") == Some(&"flowed".to_owned()) { -// text_body = Some(b); -// // text_body = Some(utils::unformat_flowed(&b)); -// } else { -// text_body = Some(b); -// } -// } -// if top.ctype.mimetype == "text/html" { -// html_body = Some(nanohtml2text::html2text( -// &top.get_body().unwrap_or(nobody.to_owned()), -// )); -// } -// } -// } -// if let Some(b) = text_body { -// body = b; -// mime = "text/plain".to_owned(); -// } else if let Some(b) = html_body { -// body = b; -// mime = "text/html".to_owned(); -// } -// let headers = &parsed_mail.headers; -// let id = headers -// .get_first_value("message-id") -// .and_then(|m| { -// msgidparse(&m).ok().and_then(|i| match i.len() { -// 0 => None, -// _ => Some(i[0].clone()), -// }) -// }) -// .context("No valid message ID")?; -// // Assume 1 in-reply-to header. a reasonable assumption -// let in_reply_to = headers.get_first_value("in-reply-to").and_then(|m| { -// msgidparse(&m).ok().and_then(|i| match i.len() { -// 0 => None, -// _ => Some(i[0].clone()), -// }) -// }); -// let subject = headers -// .get_first_value("subject") -// .unwrap_or("(no subject)".to_owned()); -// // TODO move upstream, add key/value parsing -// // https://datatracker.ietf.org/doc/html/rfc2822.html#section-3.6.7 -// // https://github.com/staktrace/mailparse/issues/99 -// let received = headers.get_first_value("received"); -// let date_string = match received { -// Some(r) => { -// let s: Vec<&str> = r.split(";").collect(); -// s[s.len() - 1].to_owned() -// } -// None => headers.get_first_value("date").context("No date header")?, -// }; - -// let date = dateparse(&date_string)? as u64; -// let from = addrparse_header(headers.get_first_header("from").context("No from header")?)? -// .extract_single_info() -// .context("Could not parse from header")?; - -// return Ok(Email { -// id, -// in_reply_to, -// from, -// subject, -// date, -// date_string, -// body, -// mime, -// }); -// } - // fn write_index(lists: Vec<String>) -> Result<()> { // let description = &Config::global().description; // let tmp = html! { @@ -309,188 +206,3 @@ fn main() -> Result<()> { // layout("Mail Archives".to_string(), tmp).write_to_io(&mut br)?; // Ok(()) // } - -// // fn oldmain() -> Result<()> { -// // let args = arg::Args::from_env(); - -// // let mut config = Config::from_file(&args.config)?; -// // config.out_dir = args.out_dir; -// // config.relative_times = args.flags.contains('r'); -// // config.include_raw = args.flags.contains('R'); -// // INSTANCE.set(config).unwrap(); - -// // // let is_subfolder = std::fs::read_dir(&args.maildir) -// // // .unwrap() -// // // .any(|a| a.unwrap().file_name().to_str().unwrap() == "cur"); - -// // let css = include_bytes!("style.css"); -// // let mut names = vec![]; -// // let mut message_count = 0; -// // for maildir in std::fs::read_dir(&args.maildir).unwrap() { -// // let maildir = maildir?; -// // let file_name = maildir.file_name(); -// // let config = Config::global(); -// // let out_dir = config.out_dir.join(&file_name); -// // std::fs::create_dir(&out_dir).ok(); -// // let dirreader = Maildir::from(maildir.path().to_str().unwrap()); -// // let list_name = file_name.into_string().unwrap(); -// // // filter out maildir internal junk -// // if list_name.as_bytes()[0] == b'.' || ["cur", "new", "tmp"].contains(&list_name.as_str()) { -// // continue; -// // } - -// // let path = out_dir.join("messages"); -// // std::fs::remove_dir_all(&path).ok(); -// // names.push(list_name.clone()); -// // // new world WIP -// // // 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?); -// // // } -// // // threader.finalize(); -// // // return Ok(()); - -// // let mut thread_index: HashMap<String, Vec<String>> = HashMap::new(); - -// // let mut email_index: HashMap<String, Email> = HashMap::new(); -// // for entry in dirreader.list_cur().chain(dirreader.list_new()) { -// // let mut tmp = entry.unwrap(); -// // let buffer = tmp.parsed()?; -// // // persist raw messages -// // let email = match local_parse_email(&buffer) { -// // Ok(e) => e, -// // Err(e) => { -// // eprintln!("Error parsing {:?} -- {:?}", tmp.path(), e); -// // continue; -// // } -// // }; -// // message_count += 1; -// // // write raw emails -// // if Config::global().include_raw { -// // // inefficient here -- no diff -// // std::fs::create_dir(&path).ok(); -// // let mut file = File::create(out_dir.join("messages").join(email.hash()))?; -// // write_parsed_mail(&buffer, &mut file)?; -// // } -// // // 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(); -// // let thread_dir = out_dir.join("threads"); -// // std::fs::create_dir_all(&thread_dir).ok(); -// // let mut threads = vec![]; -// // let mut curr_threads = get_current_threads(&thread_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 -// // list_name: list_name.clone(), -// // }; - -// // 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 = thread_dir.join(format!("{}.html", leftover)); -// // std::fs::remove_file(&file_to_remove).ok(); -// // let file_to_remove = thread_dir.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::new(threads, &list_name); -// // list.write_to_file()?; -// // list.write_atom_feed()?; -// // // kinda clunky -// // 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)?; -// // } -// // let mut css_root = File::create(Config::global().out_dir.join("style.css"))?; -// // css_root.write(css)?; -// // write_index(names)?; -// // eprintln!("Processed {} emails", message_count); -// // Ok(()) -// // } diff --git a/src/models.rs b/src/models.rs @@ -127,6 +127,7 @@ impl StrMessage { // wonky pub fn export_eml(&self) -> Vec<u8> { let mut message = MessageBuilder::new(); + println!("{}", self.flowed); if self.flowed { message.format_flowed(); } @@ -223,7 +224,7 @@ impl StrMessage { cc: cc, date: date.to_owned(), body: body.to_string(), - flowed: false, + flowed, in_reply_to: in_reply_to, } } diff --git a/src/templates/html.rs b/src/templates/html.rs @@ -215,7 +215,7 @@ impl Thread { ("date", &x(&msg.date)), ("in_reply_to", &in_reply_to), ("extra_headers", &extra_headers), - ("body", &email_body(&msg.body)), + ("body", &email_body(&msg.body, msg.flowed)), ], ) .unwrap(), @@ -269,8 +269,14 @@ data:image/svg+xml,<?xml version="1.0" encoding="UTF-8"?> // partly stolen from // https://github.com/robinst/linkify/blob/demo/src/lib.rs#L5 // Dual licensed under MIT and Apache -pub fn email_body(body: &str) -> String { +pub fn email_body(body: &str, flowed: bool) -> String { + let mut body = body; let mut bytes = Vec::new(); + let mut tmp = String::new(); + if flowed { + tmp = unformat_flowed(body); + body = &tmp + } let mut in_reply: bool = false; for line in body.lines() { if line.starts_with(">") || (line.starts_with("On ") && line.ends_with("wrote:")) {