diff --git a/server/src/graphql.rs b/server/src/graphql.rs index 11534d6..314fe51 100644 --- a/server/src/graphql.rs +++ b/server/src/graphql.rs @@ -77,6 +77,7 @@ impl UnhandledContentType { #[derive(Debug)] pub struct PlainText { text: String, + content_tree: String, } #[Object] @@ -84,11 +85,15 @@ impl PlainText { async fn contents(&self) -> &str { &self.text } + async fn content_tree(&self) -> &str { + &self.content_tree + } } #[derive(Debug)] pub struct Html { html: String, + content_tree: String, } #[Object] @@ -96,6 +101,9 @@ impl Html { async fn contents(&self) -> &str { &self.html } + async fn content_tree(&self) -> &str { + &self.content_tree + } } #[derive(Debug, Union)] @@ -105,6 +113,21 @@ pub enum Body { Html(Html), } +impl Body { + fn html(html: String) -> Body { + Body::Html(Html { + html, + content_tree: "".to_string(), + }) + } + fn text(text: String) -> Body { + Body::PlainText(PlainText { + text, + content_tree: "".to_string(), + }) + } +} + #[derive(Debug, SimpleObject)] pub struct Email { pub name: Option, @@ -216,6 +239,12 @@ impl QueryRoot { // TODO(wathiede): normalize all email addresses through an address book with preferred // display names (that default to the most commonly seen name). let nm = ctx.data_unchecked::(); + let debug_content_tree = ctx + .look_ahead() + .field("messages") + .field("body") + .field("contentTree") + .exists(); let mut messages = Vec::new(); for path in nm.files(&thread_id)? { let path = path?; @@ -243,8 +272,21 @@ impl QueryRoot { .get_first_value("date") .and_then(|d| mailparse::dateparse(&d).ok()); let body = match extract_body(&m)? { - Body::Html(Html { html }) => Body::Html(Html { + Body::PlainText(PlainText { text, content_tree }) => Body::PlainText(PlainText { + text, + content_tree: if debug_content_tree { + render_content_type_tree(&m) + } else { + content_tree + }, + }), + Body::Html(Html { html, content_tree }) => Body::Html(Html { html: ammonia::clean(&html), + content_tree: if debug_content_tree { + render_content_type_tree(&m) + } else { + content_tree + }, }), b => b, }; @@ -274,8 +316,8 @@ impl QueryRoot { fn extract_body(m: &ParsedMail) -> Result { let body = m.get_body()?; let ret = match m.ctype.mimetype.as_str() { - "text/plain" => return Ok(Body::PlainText(PlainText { text: body })), - "text/html" => return Ok(Body::Html(Html { html: body })), + "text/plain" => return Ok(Body::text(body)), + "text/html" => return Ok(Body::html(body)), "multipart/mixed" => extract_mixed(m), "multipart/alternative" => extract_alternative(m), _ => extract_unhandled(m), @@ -301,13 +343,13 @@ fn extract_alternative(m: &ParsedMail) -> Result { for sp in &m.subparts { if sp.ctype.mimetype == "text/html" { let body = sp.get_body()?; - return Ok(Body::Html(Html { html: body })); + return Ok(Body::html(body)); } } for sp in &m.subparts { if sp.ctype.mimetype == "text/plain" { let body = sp.get_body()?; - return Ok(Body::PlainText(PlainText { text: body })); + return Ok(Body::text(body)); } } Err("extract_alternative".into()) @@ -327,8 +369,8 @@ fn extract_mixed(m: &ParsedMail) -> Result { for sp in &m.subparts { let body = sp.get_body()?; match sp.ctype.mimetype.as_str() { - "text/plain" => return Ok(Body::PlainText(PlainText { text: body })), - "text/html" => return Ok(Body::Html(Html { html: body })), + "text/plain" => return Ok(Body::text(body)), + "text/html" => return Ok(Body::html(body)), _ => (), } } @@ -340,13 +382,13 @@ fn extract_related(m: &ParsedMail) -> Result { for sp in &m.subparts { if sp.ctype.mimetype == "text/html" { let body = sp.get_body()?; - return Ok(Body::Html(Html { html: body })); + return Ok(Body::html(body)); } } for sp in &m.subparts { if sp.ctype.mimetype == "text/plain" { let body = sp.get_body()?; - return Ok(Body::PlainText(PlainText { text: body })); + return Ok(Body::text(body)); } } Err("extract_related".into()) diff --git a/web/graphql/schema.json b/web/graphql/schema.json index c60a30c..50d3495 100644 --- a/web/graphql/schema.json +++ b/web/graphql/schema.json @@ -159,6 +159,22 @@ "ofType": null } } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "contentTree", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } } ], "inputFields": null, @@ -400,6 +416,22 @@ "ofType": null } } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "contentTree", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } } ], "inputFields": null, diff --git a/web/graphql/show_thread.graphql b/web/graphql/show_thread.graphql index 163bd06..4c6af64 100644 --- a/web/graphql/show_thread.graphql +++ b/web/graphql/show_thread.graphql @@ -23,9 +23,11 @@ query ShowThreadQuery($threadId: String!) { } ... on PlainText { contents + contentTree } ... on Html { contents + contentTree } } path diff --git a/web/src/lib.rs b/web/src/lib.rs index a12bdc8..7239e4d 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -985,58 +985,57 @@ fn view_search_pager_legacy(start: usize, count: usize, total: usize) -> Node &Option; - fn addr(&self) -> &Option; + fn name(&self) -> Option<&str>; + fn addr(&self) -> Option<&str>; } impl Email for &'_ T { - fn name(&self) -> &Option { + fn name(&self) -> Option<&str> { return (*self).name(); } - fn addr(&self) -> &Option { + fn addr(&self) -> Option<&str> { return (*self).addr(); } } impl Email for ShowThreadQueryThreadMessagesCc { - fn name(&self) -> &Option { - return &self.name; + fn name(&self) -> Option<&str> { + self.name.as_deref() } - fn addr(&self) -> &Option { - return &self.addr; + fn addr(&self) -> Option<&str> { + self.addr.as_deref() } } impl Email for ShowThreadQueryThreadMessagesFrom { - fn name(&self) -> &Option { - return &self.name; + fn name(&self) -> Option<&str> { + self.name.as_deref() } - fn addr(&self) -> &Option { - return &self.addr; + fn addr(&self) -> Option<&str> { + self.addr.as_deref() } } impl Email for ShowThreadQueryThreadMessagesTo { - fn name(&self) -> &Option { - return &self.name; + fn name(&self) -> Option<&str> { + self.name.as_deref() } - fn addr(&self) -> &Option { - return &self.addr; + fn addr(&self) -> Option<&str> { + self.addr.as_deref() } } -fn view_addresses(addrs: &[E]) -> Vec> { - addrs - .into_iter() - .map(|address| { - span![ - C!["tag", "is-black"], - address.addr().as_ref().map(|a| attrs! {At::Title=>a}), - address - .name() - .as_ref() - .unwrap_or(address.addr().as_ref().unwrap_or(&"(UNKNOWN)".to_string())) - ] - }) - .collect::>() +fn view_address(email: impl Email) -> Node { + span![ + C!["tag", "is-black"], + email.addr().as_ref().map(|a| attrs! {At::Title=>a}), + email + .name() + .as_ref() + .unwrap_or(&email.addr().unwrap_or("(UNKNOWN)")) + ] +} + +fn view_addresses(addrs: &[impl Email]) -> Vec> { + addrs.into_iter().map(view_address).collect::>() } fn view_thread(thread: &ShowThreadQueryThread) -> Node { @@ -1061,11 +1060,21 @@ fn view_thread(thread: &ShowThreadQueryThread) -> Node { ShowThreadQueryThreadMessagesBodyOnUnhandledContentType { contents }, ) => pre![C!["error"], contents], ShowThreadQueryThreadMessagesBody::PlainText( - ShowThreadQueryThreadMessagesBodyOnPlainText { contents }, - ) => div![C!["view-part-text-plain"], contents], + ShowThreadQueryThreadMessagesBodyOnPlainText { + contents, + content_tree, + }, + ) => div![C!["view-part-text-plain"], contents, pre![content_tree]], ShowThreadQueryThreadMessagesBody::Html( - ShowThreadQueryThreadMessagesBodyOnHtml { contents }, - ) => div![C!["view-part-text-html"], raw![contents]], + ShowThreadQueryThreadMessagesBodyOnHtml { + contents, + content_tree, + }, + ) => div![ + C!["view-part-text-html"], + raw![contents], + pre![content_tree] + ], } ], ]