Skip to content

Commit

Permalink
feat(host): Support delivery format
Browse files Browse the repository at this point in the history
If Thunderbird doesn't give us this field, we don't add it to the EML
either; if Thunderbird gives us "deliveryFormat": null, we add
Delivery-Format: [auto] (the default value) to the EML.

Note this option may only be effective when composing emails in HTML.
  • Loading branch information
Frederick888 committed Mar 10, 2023
1 parent 92697bb commit f91e157
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 12 deletions.
99 changes: 88 additions & 11 deletions src/model/messaging.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::error::Error as StdError;
use std::{io, str::FromStr};

use super::thunderbird::*;
Expand All @@ -9,6 +10,8 @@ pub const MAX_BODY_LENGTH: usize = 768 * 1024;

const HEADER_PRIORITY: &str = "X-ExtEditorR-Priority";
const HEADER_LOWER_PRIORITY: &str = "x-exteditorr-priority"; // cspell: disable-line
const HEADER_DELIVERY_FORMAT: &str = "X-ExtEditorR-Delivery-Format";
const HEADER_LOWER_DELIVERY_FORMAT: &str = "x-exteditorr-delivery-format"; // cspell: disable-line
const HEADER_ATTACH_VCARD: &str = "X-ExtEditorR-Attach-vCard";
const HEADER_LOWER_ATTACH_VCARD: &str = "x-exteditorr-attach-vcard"; // cspell: disable-line
const HEADER_SEND_ON_EXIT: &str = "X-ExtEditorR-Send-On-Exit";
Expand Down Expand Up @@ -84,6 +87,14 @@ impl Compose {
if let Some(ref priority) = self.compose_details.priority {
writeln_crlf!(w, "{}: {}", HEADER_PRIORITY, priority)?;
}
if let Some(ref delivery_format) = self.compose_details.delivery_format {
match delivery_format {
Some(delivery_format) => {
writeln_crlf!(w, "{}: [{}]", HEADER_DELIVERY_FORMAT, delivery_format)?
}
None => writeln_crlf!(w, "{}: [{}]", HEADER_DELIVERY_FORMAT, DeliveryFormat::Auto)?,
}
}
if let Some(attach_vcard) = self.compose_details.attach_vcard.inner {
writeln_crlf!(w, "{}: [{}]", HEADER_ATTACH_VCARD, attach_vcard)?;
}
Expand Down Expand Up @@ -149,8 +160,18 @@ impl Compose {
HEADER_LOWER_PRIORITY => {
self.compose_details.priority = Some(Priority::from_str(header_value)?)
}
HEADER_LOWER_DELIVERY_FORMAT => {
if let Some(delivery_format) = Self::parse_optional_header::<DeliveryFormat>(
HEADER_DELIVERY_FORMAT,
header_value,
)? {
self.compose_details.delivery_format = Some(Some(delivery_format));
}
}
HEADER_LOWER_ATTACH_VCARD => {
if let Some(attach_vcard) = Self::parse_attach_vcard(header_value)? {
if let Some(attach_vcard) =
Self::parse_optional_header::<bool>(HEADER_ATTACH_VCARD, header_value)?
{
self.compose_details.attach_vcard.set(attach_vcard);
}
}
Expand Down Expand Up @@ -256,16 +277,18 @@ impl Compose {
Ok(())
}

fn parse_attach_vcard(header_value: &str) -> Result<Option<bool>> {
fn parse_optional_header<T>(header_name: &str, header_value: &str) -> Result<Option<T>>
where
T: FromStr,
<T as FromStr>::Err: StdError + 'static,
{
match header_value {
"true" => Ok(Some(true)),
"false" => Ok(Some(false)),
"[true]" | "[false]" => Ok(None),
_ => {
let message = format!(
"ExtEditorR failed to parse {HEADER_ATTACH_VCARD} value: {header_value}"
);
Err(anyhow!(message))
_ if header_value.starts_with('[') && header_value.ends_with(']') => Ok(None),
header_value => {
let parsed = T::from_str(header_value).map_err(|_| {
anyhow!("ExtEditorR failed to parse {header_name} value: {header_value}")
})?;
Ok(Some(parsed))
}
}
}
Expand Down Expand Up @@ -467,6 +490,60 @@ pub mod tests {
assert_eq!("Hello!\r\n", &responses[2].compose_details.plain_text_body);
}

#[test]
fn merge_delivery_format_test() {
let mut request = get_blank_compose();
let mut buf = Vec::new();
let result = request.to_eml(&mut buf);
assert!(result.is_ok());
let output = String::from_utf8(buf).unwrap();
assert!(!output.contains("X-ExtEditorR-Delivery-Format:"));

request.compose_details.delivery_format = Some(None);
let mut buf = Vec::new();
let result = request.to_eml(&mut buf);
assert!(result.is_ok());
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("X-ExtEditorR-Delivery-Format: [auto]"));

request.compose_details.delivery_format = Some(Some(DeliveryFormat::Both));
let mut buf = Vec::new();
let result = request.to_eml(&mut buf);
assert!(result.is_ok());
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("X-ExtEditorR-Delivery-Format: [both]"));

let mut eml = "X-ExtEditorR-Delivery-Format: [hello]\r\n\r\nThis is a test.\r\n".as_bytes();
let responses = request.merge_from_eml(&mut eml, 512).unwrap();
assert_eq!(1, responses.len());
assert_eq!(
&DeliveryFormat::Both,
responses[0]
.compose_details
.delivery_format
.as_ref()
.unwrap()
.as_ref()
.unwrap()
);

request.compose_details.delivery_format = None;
let mut eml =
"X-ExtEditorR-Delivery-Format: plaintext\r\n\r\nThis is a test.\r\n".as_bytes();
let responses = request.merge_from_eml(&mut eml, 512).unwrap();
assert_eq!(1, responses.len());
assert_eq!(
&DeliveryFormat::PlainText,
responses[0]
.compose_details
.delivery_format
.as_ref()
.unwrap()
.as_ref()
.unwrap()
);
}

#[test]
fn merge_priority_test() {
let mut request = get_blank_compose();
Expand All @@ -488,7 +565,7 @@ pub mod tests {
}

#[test]
fn attach_vcard_test() {
fn merge_attach_vcard_test() {
let mut request = get_blank_compose();
request.compose_details.attach_vcard = TrackedOptionBool::new(false);

Expand Down
61 changes: 60 additions & 1 deletion src/model/thunderbird.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::{anyhow, Result};
use serde::{de::Visitor, Deserialize, Serialize};
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
use strum::{Display, EnumString};

pub trait EmailHeaderValue {
Expand Down Expand Up @@ -67,6 +67,13 @@ pub struct ComposeDetails {
pub follow_up_to: ComposeRecipientList,
pub newsgroups: Newsgroups,
pub subject: String,
#[serde(
default,
rename = "deliveryFormat",
deserialize_with = "deserialize_some",
skip_serializing_if = "is_none_nested"
)]
pub delivery_format: Option<Option<DeliveryFormat>>,
#[serde(rename = "isPlainText")]
pub is_plain_text: bool,
#[serde(skip_serializing_if = "String::is_empty")]
Expand Down Expand Up @@ -210,6 +217,16 @@ impl Serialize for TrackedOptionBool {
}
}

#[derive(Clone, Display, Debug, PartialEq, Eq, Deserialize, Serialize, EnumString)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum DeliveryFormat {
Auto,
PlainText,
Html,
Both,
}

#[derive(Clone, Display, Debug, PartialEq, Eq, Deserialize, Serialize, EnumString)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
Expand Down Expand Up @@ -313,6 +330,20 @@ pub enum Newsgroups {
Multiple(Vec<String>),
}

// /~https://github.com/serde-rs/serde/issues/984#issuecomment-314143738
// Any value that is present is considered Some value, including null.
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>,
{
Deserialize::deserialize(deserializer).map(Some)
}

fn is_none_nested<T>(v: &Option<Option<T>>) -> bool {
matches!(v, None | Some(None))
}

#[cfg(test)]
pub mod tests {
use super::*;
Expand Down Expand Up @@ -468,6 +499,33 @@ pub mod tests {
}
}

#[test]
fn compose_details_delivery_format_test() {
let mut compose_details = get_blank_compose_details();
compose_details.body = "<html>".to_owned();
compose_details.plain_text_body = "hello, world!".to_owned();
let json = serde_json::to_string(&compose_details).unwrap();
assert!(!json.contains("deliveryFormat"));

compose_details = serde_json::from_str(&json).unwrap();
assert_eq!(None, compose_details.delivery_format);

compose_details.delivery_format = Some(None);
let json = serde_json::to_string(&compose_details).unwrap();
assert!(!json.contains("deliveryFormat"));

let json = json[..json.len() - 1].to_string() + r#","deliveryFormat":"plaintext"}"#;
compose_details = serde_json::from_str(&json).unwrap();
assert_eq!(
Some(Some(DeliveryFormat::PlainText)),
compose_details.delivery_format
);

compose_details.delivery_format = Some(Some(DeliveryFormat::Html));
let json = serde_json::to_string(&compose_details).unwrap();
assert!(json.contains(r#""deliveryFormat":"html""#));
}

#[test]
fn tracked_option_bool_test() {
let mut tracked_option_bool = TrackedOptionBool::default();
Expand Down Expand Up @@ -521,6 +579,7 @@ pub mod tests {
follow_up_to: ComposeRecipientList::Multiple(Vec::new()),
newsgroups: Newsgroups::Multiple(Vec::new()),
subject: "".to_owned(),
delivery_format: None,
is_plain_text: true,
body: "".to_owned(),
plain_text_body: "".to_owned(),
Expand Down

0 comments on commit f91e157

Please sign in to comment.