linkify URLs in plaintext emails.

This commit is contained in:
Bill Thiede 2024-02-03 11:10:51 -08:00
parent 569781b592
commit 568d83f029
4 changed files with 34 additions and 5 deletions

10
Cargo.lock generated
View File

@ -1747,6 +1747,15 @@ version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
name = "linkify"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.13" version = "0.4.13"
@ -3228,6 +3237,7 @@ dependencies = [
"async-graphql-rocket", "async-graphql-rocket",
"css-inline", "css-inline",
"glog", "glog",
"linkify",
"log 0.4.20", "log 0.4.20",
"lol_html", "lol_html",
"mailparse", "mailparse",

View File

@ -27,6 +27,7 @@ lol_html = "1.2.0"
css-inline = "0.13.0" css-inline = "0.13.0"
anyhow = "1.0.79" anyhow = "1.0.79"
maplit = "1.0.2" maplit = "1.0.2"
linkify = "0.10.0"
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
version = "0.4.11" version = "0.4.11"

View File

@ -16,7 +16,7 @@ use memmap::MmapOptions;
use notmuch::Notmuch; use notmuch::Notmuch;
use rocket::time::Instant; use rocket::time::Instant;
use crate::sanitize_html; use crate::{linkify_html, sanitize_html};
pub struct QueryRoot; pub struct QueryRoot;
@ -347,8 +347,11 @@ impl QueryRoot {
.get_first_value("date") .get_first_value("date")
.and_then(|d| mailparse::dateparse(&d).ok()); .and_then(|d| mailparse::dateparse(&d).ok());
let body = match extract_body(&m)? { let body = match extract_body(&m)? {
Body::PlainText(PlainText { text, content_tree }) => Body::PlainText(PlainText { Body::PlainText(PlainText { text, content_tree }) => Body::Html(Html {
text, html: format!(
r#"<p class="view-part-text-plain">{}</p>"#,
linkify_html(&text)
),
content_tree: if debug_content_tree { content_tree: if debug_content_tree {
render_content_type_tree(&m) render_content_type_tree(&m)
} else { } else {

View File

@ -3,6 +3,7 @@ pub mod graphql;
pub mod nm; pub mod nm;
use css_inline::{CSSInliner, InlineError, InlineOptions}; use css_inline::{CSSInliner, InlineError, InlineOptions};
use linkify::{LinkFinder, LinkKind};
use log::error; use log::error;
use lol_html::{element, errors::RewritingError, rewrite_str, RewriteStrSettings}; use lol_html::{element, errors::RewritingError, rewrite_str, RewriteStrSettings};
use maplit::{hashmap, hashset}; use maplit::{hashmap, hashset};
@ -16,6 +17,22 @@ pub enum SanitizeError {
InlineError(#[from] InlineError), InlineError(#[from] InlineError),
} }
pub fn linkify_html(text: &str) -> String {
let finder = LinkFinder::new();
let mut parts = Vec::new();
for span in finder.spans(text) {
// TODO(wathiede): use Cow<str>?
match span.kind() {
// Text as-is
None => parts.push(span.as_str().to_string()),
// Wrap in anchor tag
Some(LinkKind::Url) => parts.push(format!(r#"<a href="{0}">{0}</a>"#, span.as_str())),
_ => todo!("unhandled kind: {:?}", span.kind().unwrap()),
}
}
parts.join("")
}
pub fn sanitize_html(html: &str) -> Result<String, SanitizeError> { pub fn sanitize_html(html: &str) -> Result<String, SanitizeError> {
let element_content_handlers = vec![ let element_content_handlers = vec![
// Open links in new tab // Open links in new tab
@ -204,8 +221,6 @@ pub fn sanitize_html(html: &str) -> Result<String, SanitizeError> {
//let clean_html = inlined_html; //let clean_html = inlined_html;
Ok(rewrite_str( Ok(rewrite_str(
// TODO(wathiede): replace ammonia with more lol-html rules.
// &ammonia::clean(&html),
&clean_html, &clean_html,
RewriteStrSettings { RewriteStrSettings {
element_content_handlers, element_content_handlers,