From 11366b6fac9a0976ad2e93aa606bc6ec5bbef3e5 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Sun, 26 Nov 2023 16:37:29 -0800 Subject: [PATCH] web & server: implement handling for text and html bodies. --- server/src/graphql.rs | 57 +++++++++++++++ web/graphql/schema.json | 123 ++++++++++++++++++++++++++++++++ web/graphql/show_thread.graphql | 12 ++++ web/src/lib.rs | 19 +++-- 4 files changed, 204 insertions(+), 7 deletions(-) diff --git a/server/src/graphql.rs b/server/src/graphql.rs index f72a3e2..abc7eee 100644 --- a/server/src/graphql.rs +++ b/server/src/graphql.rs @@ -6,6 +6,7 @@ use std::{ use async_graphql::{ connection::{self, Connection, Edge}, Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject, + Union, }; use log::{info, warn}; use mailparse::{parse_mail, MailHeaderMap, ParsedMail}; @@ -55,6 +56,51 @@ pub struct Message { pub subject: Option, // Parsed Date header, if found and valid pub timestamp: Option, + // The body contents + pub body: Body, +} + +#[derive(Debug)] +struct UnhandledContentType { + text: String, +} + +#[Object] +impl UnhandledContentType { + async fn contents(&self) -> &str { + &self.text + } +} + +#[derive(Debug)] +struct PlainText { + text: String, +} + +#[Object] +impl PlainText { + async fn contents(&self) -> &str { + &self.text + } +} + +#[derive(Debug)] +struct Html { + html: String, +} + +#[Object] +impl Html { + async fn contents(&self) -> &str { + &self.html + } +} + +#[derive(Debug, Union)] +pub enum Body { + UnhandledContentType(UnhandledContentType), + PlainText(PlainText), + Html(Html), } #[derive(Debug, SimpleObject)] @@ -195,12 +241,23 @@ impl QueryRoot { .headers .get_first_value("date") .and_then(|d| mailparse::dateparse(&d).ok()); + let body = m.get_body()?; + let body = match m.ctype.mimetype.as_str() { + "text/plain" => Body::PlainText(PlainText { text: body }), + "text/html" => Body::Html(Html { html: body }), + _ => { + let msg = format!("Unhandled body content type: {}", m.ctype.mimetype); + warn!("{}", msg); + Body::UnhandledContentType(UnhandledContentType { text: msg }) + } + }; messages.push(Message { from, to, cc, subject, timestamp, + body, }); } messages.reverse(); diff --git a/web/graphql/schema.json b/web/graphql/schema.json index 7d70a6f..ca63315 100644 --- a/web/graphql/schema.json +++ b/web/graphql/schema.json @@ -59,6 +59,32 @@ }, "subscriptionType": null, "types": [ + { + "description": null, + "enumValues": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "kind": "UNION", + "name": "Body", + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "UnhandledContentType", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PlainText", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Html", + "ofType": null + } + ] + }, { "description": "The `Boolean` scalar type represents `true` or `false`.", "enumValues": null, @@ -114,6 +140,33 @@ "name": "Float", "possibleTypes": null }, + { + "description": null, + "enumValues": null, + "fields": [ + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "contents", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + ], + "inputFields": null, + "interfaces": [], + "kind": "OBJECT", + "name": "Html", + "possibleTypes": null + }, { "description": null, "enumValues": null, @@ -221,6 +274,22 @@ "name": "Int", "ofType": null } + }, + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "body", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "UNION", + "name": "Body", + "ofType": null + } + } } ], "inputFields": null, @@ -296,6 +365,33 @@ "name": "PageInfo", "possibleTypes": null }, + { + "description": null, + "enumValues": null, + "fields": [ + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "contents", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + ], + "inputFields": null, + "interfaces": [], + "kind": "OBJECT", + "name": "PlainText", + "possibleTypes": null + }, { "description": null, "enumValues": null, @@ -865,6 +961,33 @@ "name": "ThreadSummaryEdge", "possibleTypes": null }, + { + "description": null, + "enumValues": null, + "fields": [ + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "contents", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + ], + "inputFields": null, + "interfaces": [], + "kind": "OBJECT", + "name": "UnhandledContentType", + "possibleTypes": null + }, { "description": "A Directive provides a way to describe alternate runtime execution and type\nvalidation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution\nbehavior in ways field arguments will not suffice, such as conditionally\nincluding or skipping a field. Directives provide this by describing\nadditional information to the executor.", "enumValues": null, diff --git a/web/graphql/show_thread.graphql b/web/graphql/show_thread.graphql index 6292f08..6ce4f30 100644 --- a/web/graphql/show_thread.graphql +++ b/web/graphql/show_thread.graphql @@ -16,6 +16,18 @@ query ShowThreadQuery($threadId: String!) { addr } timestamp + body { + __typename + ... on UnhandledContentType { + contents + } + ... on PlainText { + contents + } + ... on Html { + contents + } + } } } tags { diff --git a/web/src/lib.rs b/web/src/lib.rs index ea99b60..dec57c7 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -1054,15 +1054,20 @@ fn view_thread(thread: &ShowThreadQueryThread) -> Node { .map(|ts| div![C!["header"], "Date: ", human_age(ts)]), IF!(!msg.to.is_empty() => div![C!["header"], "To: ", view_addresses(&msg.to)]), IF!(!msg.cc.is_empty() => div![C!["header"], "CC: ", view_addresses(&msg.cc)]), - /* div![ - C!["body"], - match &message.body { - Some(body) => view_body(body.as_slice()), - None => div![""], - }, + C!["body"], + match &msg.body { + ShowThreadQueryThreadMessagesBody::UnhandledContentType( + ShowThreadQueryThreadMessagesBodyOnUnhandledContentType { contents }, + ) => div![C!["error"], contents], + ShowThreadQueryThreadMessagesBody::PlainText( + ShowThreadQueryThreadMessagesBodyOnPlainText { contents }, + ) => div![C!["view-part-text-plain"], contents], + ShowThreadQueryThreadMessagesBody::Html( + ShowThreadQueryThreadMessagesBodyOnHtml { contents }, + ) => div![C!["view-part-text-html"], raw![contents]], + } ], - */ ] }); div![