crabmail

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

commit 1c983c06061f13c5b22357e49157d29a593bd9a0
parent 181636084d041db7bbbfef12465b6fdc70f40a3e
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Tue, 14 Dec 2021 21:32:37 -0800

Add parsing to email body

Diffstat:
MCargo.lock | 10++++++++++
MCargo.toml | 15++++++++-------
Msrc/filters.rs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtemplates/static/style.css | 3+++
Mtemplates/thread.html | 2+-
5 files changed, 84 insertions(+), 8 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -153,6 +153,7 @@ dependencies = [ "anyhow", "askama", "hex", + "linkify", "mailparse", "mbox-reader", "once_cell", @@ -307,6 +308,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] +name = "linkify" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccbcd666d915aa3ae3c3774999a9e20b2776a018309b8159d07df062b91f45e8" +dependencies = [ + "memchr", +] + +[[package]] name = "log" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -11,14 +11,15 @@ default = ["html"] html = ["ammonia"] [dependencies] -mailparse = "0.13" -url = "2" -once_cell = "1.9" +ammonia = {version = "3", optional = true} +anyhow = "1.0" +askama = "0.10" hex = "0.4" -sha3 = "0.10" +linkify = "0.8.0" +mailparse = "0.13" mbox-reader = "0.2.0" #unamaintained, should remove dep +once_cell = "1.9" pico-args = "0.4.1" -askama = "0.10" -anyhow = "1.0" quick-xml = "0.22" -ammonia = {version = "3", optional = true} +sha3 = "0.10" +url = "2" diff --git a/src/filters.rs b/src/filters.rs @@ -1,3 +1,4 @@ +use linkify::{LinkFinder, LinkKind}; use std::time::{SystemTime, UNIX_EPOCH}; pub fn time_ago(amount: &u64) -> askama::Result<String> { @@ -9,6 +10,67 @@ const SOLAR_YEAR_SECS: u64 = 31556926; // add <span> for lines starting with > to make them grey // parse hyperlinks for you +// stolen from +// https://github.com/robinst/linkify/blob/demo/src/lib.rs#L5 + +pub fn email_body(body: &str) -> askama::Result<String> { + let mut bytes = Vec::new(); + let mut in_reply: bool = false; + for line in body.lines() { + if line.starts_with(">") || (line.starts_with("On ") && line.ends_with("wrote:")) { + if !in_reply { + in_reply = true; + bytes.extend_from_slice(b"<span class='reply-text'>"); + } + } else if in_reply { + bytes.extend_from_slice(b"</span>"); + in_reply = false + } + + let mut finder = LinkFinder::new(); + for span in finder.spans(line) { + match span.kind() { + Some(LinkKind::Url) => { + bytes.extend_from_slice(b"<a href=\""); + escape(span.as_str(), &mut bytes); + bytes.extend_from_slice(b"\">"); + escape(span.as_str(), &mut bytes); + bytes.extend_from_slice(b"</a>"); + } + Some(LinkKind::Email) => { + bytes.extend_from_slice(b"<a href=\"mailto:"); + escape(span.as_str(), &mut bytes); + bytes.extend_from_slice(b"\">"); + escape(span.as_str(), &mut bytes); + bytes.extend_from_slice(b"</a>"); + } + _ => { + escape(span.as_str(), &mut bytes); + } + } + } + bytes.extend(b"\n"); + } + if in_reply { + bytes.extend_from_slice(b"</span>"); + } + // TODO err conversion + Ok(String::from_utf8(bytes).expect("not utf8")) +} + +fn escape(text: &str, dest: &mut Vec<u8>) { + for c in text.bytes() { + match c { + b'&' => dest.extend_from_slice(b"&amp;"), + b'<' => dest.extend_from_slice(b"&lt;"), + b'>' => dest.extend_from_slice(b"&gt;"), + b'"' => dest.extend_from_slice(b"&quot;"), + b'\'' => dest.extend_from_slice(b"&#39;"), + _ => dest.push(c), + } + } +} + fn timeago(unixtime: u64) -> String { let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/templates/static/style.css b/templates/static/style.css @@ -40,6 +40,9 @@ table { border-spacing: 0.5em 0.1em; } color: grey; } +.reply-text { + color: grey; +} .email-body { white-space:pre-line; font-family:monospace; diff --git a/templates/thread.html b/templates/thread.html @@ -14,7 +14,7 @@ {% match message.in_reply_to %}{% when Some with (replies_to) %} <a title="replies-to" href="#{{replies_to}}">Re:</a>{%when none %}{% endmatch %} <div class="email-body"> - {{message.body}} + {{message.body | email_body | safe }} </div> <div class="right"> <a href="{{message.mailto()}}">✉️ reply</a>