diff --git a/Cargo.toml b/Cargo.toml index dd7d52f..a28e8e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,8 @@ chrono = { version = "0.4", features = ["serde"]} reqwest = "0.11" base64 = "0.13" -#[dev-dependencies] +[features] +debug = [] [profile.bench] debug = true \ No newline at end of file diff --git a/src/blob/download.rs b/src/blob/download.rs index a472987..c58cff3 100644 --- a/src/blob/download.rs +++ b/src/blob/download.rs @@ -5,7 +5,8 @@ use reqwest::header::CONTENT_TYPE; use crate::{client::Client, core::session::URLPart}; impl Client { - pub async fn download(&self, account_id: &str, blob_id: &str) -> crate::Result> { + pub async fn download(&self, blob_id: &str) -> crate::Result> { + let account_id = self.default_account_id(); let mut download_url = String::with_capacity( self.session().download_url().len() + account_id.len() + blob_id.len(), ); diff --git a/src/blob/upload.rs b/src/blob/upload.rs index e8bd1fb..f1fc33d 100644 --- a/src/blob/upload.rs +++ b/src/blob/upload.rs @@ -23,10 +23,10 @@ pub struct UploadResponse { impl Client { pub async fn upload( &self, - account_id: &str, - content_type: Option<&str>, blob: Vec, + content_type: Option<&str>, ) -> crate::Result { + let account_id = self.default_account_id(); let mut upload_url = String::with_capacity(self.session().upload_url().len() + account_id.len()); diff --git a/src/client.rs b/src/client.rs index 9b1ac17..eb41c8f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -80,8 +80,9 @@ impl Client { }) } - pub fn set_timeout(&mut self, timeout: u64) { + pub fn set_timeout(&mut self, timeout: u64) -> &mut Self { self.timeout = timeout; + self } pub fn timeout(&self) -> u64 { @@ -143,8 +144,9 @@ impl Client { Ok(response) } - pub fn set_default_account_id(&mut self, defaul_account_id: impl Into) { + pub fn set_default_account_id(&mut self, defaul_account_id: impl Into) -> &mut Self { self.default_account_id = defaul_account_id.into(); + self } pub fn default_account_id(&self) -> &str { @@ -186,8 +188,23 @@ impl Client { #[cfg(test)] mod tests { + use crate::email::{EmailBodyPart, Header, Property}; - fn _test_serialize() { + #[test] + fn test_serialize() { + println!( + "{:?}", + serde_json::from_slice::( + br#"{ + "partId": "0", + "header:X-Custom-Header": "123", + "type": "text/html", + "charset": "us-ascii", + "size": 175 + }"# + ) + .unwrap() + ); /*let coco = request .send() diff --git a/src/core/query.rs b/src/core/query.rs index 8def293..1ad4f90 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -14,21 +14,24 @@ pub struct QueryRequest { sort: Option>>, #[serde(rename = "position")] - position: i32, + #[serde(skip_serializing_if = "Option::is_none")] + position: Option, #[serde(rename = "anchor")] #[serde(skip_serializing_if = "Option::is_none")] anchor: Option, #[serde(rename = "anchorOffset")] - anchor_offset: i32, + #[serde(skip_serializing_if = "Option::is_none")] + anchor_offset: Option, #[serde(rename = "limit")] #[serde(skip_serializing_if = "Option::is_none")] limit: Option, #[serde(rename = "calculateTotal")] - calculate_total: bool, + #[serde(skip_serializing_if = "Option::is_none")] + calculate_total: Option, #[serde(flatten)] arguments: A, @@ -78,7 +81,7 @@ pub struct QueryResponse { query_state: String, #[serde(rename = "canCalculateChanges")] - can_calculate_changes: bool, + can_calculate_changes: Option, #[serde(rename = "position")] position: i32, @@ -99,11 +102,11 @@ impl QueryRequest { account_id, filter: None, sort: None, - position: 0, + position: None, anchor: None, - anchor_offset: 0, + anchor_offset: None, limit: None, - calculate_total: false, + calculate_total: None, arguments: A::default(), } } @@ -124,7 +127,7 @@ impl QueryRequest { } pub fn position(&mut self, position: i32) -> &mut Self { - self.position = position; + self.position = position.into(); self } @@ -134,7 +137,7 @@ impl QueryRequest { } pub fn anchor_offset(&mut self, anchor_offset: i32) -> &mut Self { - self.anchor_offset = anchor_offset; + self.anchor_offset = anchor_offset.into(); self } @@ -174,7 +177,7 @@ impl QueryResponse { } pub fn can_calculate_changes(&self) -> bool { - self.can_calculate_changes + self.can_calculate_changes.unwrap_or(false) } } diff --git a/src/email/get.rs b/src/email/get.rs index 5957bed..b13c21a 100644 --- a/src/email/get.rs +++ b/src/email/get.rs @@ -1,7 +1,8 @@ use crate::Get; use super::{ - Email, EmailAddress, EmailAddressGroup, EmailBodyPart, EmailBodyValue, EmailHeader, Field, + Email, EmailAddress, EmailAddressGroup, EmailBodyPart, EmailBodyValue, EmailHeader, Header, + HeaderValue, }; impl Email { @@ -109,12 +110,17 @@ impl Email { *self.has_attachment.as_ref().unwrap_or(&false) } - pub fn header(&self, id: &str) -> Option<&Field> { - self.others.get(id).and_then(|v| v.as_ref()) + pub fn header(&self, id: &Header) -> Option<&HeaderValue> { + self.headers.get(id).and_then(|v| v.as_ref()) } - pub fn has_header(&self, id: &str) -> bool { - self.others.contains_key(id) + pub fn has_header(&self, id: &Header) -> bool { + self.headers.contains_key(id) + } + + #[cfg(feature = "debug")] + pub fn into_test(self) -> super::TestEmail { + self.into() } } @@ -135,6 +141,10 @@ impl EmailBodyPart { self.headers.as_deref() } + pub fn header(&self, id: &Header) -> Option<&HeaderValue> { + self.header.as_ref().and_then(|v| v.get(id)) + } + pub fn name(&self) -> Option<&str> { self.name.as_deref() } diff --git a/src/email/helpers.rs b/src/email/helpers.rs index f9ce9cd..e5a478a 100644 --- a/src/email/helpers.rs +++ b/src/email/helpers.rs @@ -6,7 +6,9 @@ use crate::{ }, }; -use super::{import::EmailImportResponse, Email, Property}; +use super::{ + import::EmailImportResponse, parse::EmailParseResponse, BodyProperty, Email, Property, +}; impl Client { pub async fn email_import( @@ -20,10 +22,7 @@ impl Client { T: IntoIterator, U: Into, { - let blob_id = self - .upload(self.default_account_id(), None, raw_message) - .await? - .unwrap_blob_id(); + let blob_id = self.upload(raw_message, None).await?.unwrap_blob_id(); let mut request = self.build(); let import_request = request .import_email() @@ -107,12 +106,12 @@ impl Client { pub async fn email_get( &mut self, id: &str, - properties: Option>, + properties: Option>, ) -> crate::Result> { let mut request = self.build(); let get_request = request.get_email().ids([id]); if let Some(properties) = properties { - get_request.properties(properties.into_iter()); + get_request.properties(properties); } request .send_single::() @@ -135,4 +134,33 @@ impl Client { } request.send_single::().await } + + pub async fn email_parse( + &mut self, + blob_id: &str, + properties: Option>, + body_properties: Option>, + max_body_value_bytes: Option, + ) -> crate::Result { + let mut request = self.build(); + let parse_request = request.parse_email().blob_ids([blob_id]); + if let Some(properties) = properties { + parse_request.properties(properties); + } + + if let Some(body_properties) = body_properties { + parse_request.body_properties(body_properties); + } + + if let Some(max_body_value_bytes) = max_body_value_bytes { + parse_request + .fetch_all_body_values(true) + .max_body_value_bytes(max_body_value_bytes); + } + + request + .send_single::() + .await + .and_then(|mut r| r.parsed(blob_id)) + } } diff --git a/src/email/mod.rs b/src/email/mod.rs index c42b0e0..a447741 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -7,7 +7,7 @@ pub mod search_snippet; pub mod set; use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{de::Visitor, Deserialize, Serialize}; use std::{ collections::HashMap, fmt::{self, Display, Formatter}, @@ -56,47 +56,67 @@ pub struct Email { #[serde(skip_serializing_if = "Option::is_none")] received_at: Option>, - #[serde(rename = "messageId", alias = "header:Message-ID:asMessageIds")] + #[cfg_attr( + not(feature = "debug"), + serde(alias = "header:Message-ID:asMessageIds") + )] + #[serde(rename = "messageId")] #[serde(skip_serializing_if = "Option::is_none")] message_id: Option>, - #[serde(rename = "inReplyTo", alias = "header:In-Reply-To:asMessageIds")] + #[serde(rename = "inReplyTo")] + #[cfg_attr( + not(feature = "debug"), + serde(alias = "header:In-Reply-To:asMessageIds") + )] #[serde(skip_serializing_if = "Option::is_none")] in_reply_to: Option>, - #[serde(rename = "references", alias = "header:References:asMessageIds")] + #[serde(rename = "references")] + #[cfg_attr( + not(feature = "debug"), + serde(alias = "header:References:asMessageIds") + )] #[serde(skip_serializing_if = "Option::is_none")] references: Option>, - #[serde(rename = "sender", alias = "header:Sender:asAddresses")] + #[serde(rename = "sender")] + #[cfg_attr(not(feature = "debug"), serde(alias = "header:Sender:asAddresses"))] #[serde(skip_serializing_if = "Option::is_none")] sender: Option>, - #[serde(rename = "from", alias = "header:From:asAddresses")] + #[serde(rename = "from")] + #[cfg_attr(not(feature = "debug"), serde(alias = "header:From:asAddresses"))] #[serde(skip_serializing_if = "Option::is_none")] from: Option>, - #[serde(rename = "to", alias = "header:To:asAddresses")] + #[serde(rename = "to")] + #[cfg_attr(not(feature = "debug"), serde(alias = "header:To:asAddresses"))] #[serde(skip_serializing_if = "Option::is_none")] to: Option>, - #[serde(rename = "cc", alias = "header:Cc:asAddresses")] + #[serde(rename = "cc")] + #[cfg_attr(not(feature = "debug"), serde(alias = "header:Cc:asAddresses"))] #[serde(skip_serializing_if = "Option::is_none")] cc: Option>, - #[serde(rename = "bcc", alias = "header:Bcc:asAddresses")] + #[serde(rename = "bcc")] + #[cfg_attr(not(feature = "debug"), serde(alias = "header:Bcc:asAddresses"))] #[serde(skip_serializing_if = "Option::is_none")] bcc: Option>, - #[serde(rename = "replyTo", alias = "header:Reply-To:asAddresses")] + #[serde(rename = "replyTo")] + #[cfg_attr(not(feature = "debug"), serde(alias = "header:Reply-To:asAddresses"))] #[serde(skip_serializing_if = "Option::is_none")] reply_to: Option>, - #[serde(rename = "subject", alias = "header:Subject:asText")] + #[serde(rename = "subject")] + #[cfg_attr(not(feature = "debug"), serde(alias = "header:Subject:asText"))] #[serde(skip_serializing_if = "Option::is_none")] subject: Option, - #[serde(rename = "sentAt", alias = "header:Date:asDate")] + #[serde(rename = "sentAt")] + #[cfg_attr(not(feature = "debug"), serde(alias = "header:Date:asDate"))] #[serde(skip_serializing_if = "Option::is_none")] sent_at: Option>, @@ -130,7 +150,12 @@ pub struct Email { #[serde(flatten)] #[serde(skip_serializing_if = "HashMap::is_empty")] - others: HashMap>, + headers: HashMap>, + + #[serde(flatten)] + #[serde(skip_deserializing)] + #[serde(skip_serializing_if = "HashMap::is_empty")] + patch: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -185,6 +210,10 @@ pub struct EmailBodyPart { #[serde(rename = "subParts")] #[serde(skip_serializing_if = "Option::is_none")] sub_parts: Option>, + + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + header: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -202,17 +231,6 @@ pub struct EmailBodyValue { is_truncated: bool, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum Field { - Text(String), - TextList(Vec), - Date(DateTime), - Addresses(Vec), - GroupedAddresses(Vec), - Bool(bool), -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailAddress { #[serde(skip)] @@ -240,86 +258,100 @@ pub struct EmailHeader { value: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub enum Property { - #[serde(rename = "id")] Id, - #[serde(rename = "blobId")] BlobId, - #[serde(rename = "threadId")] ThreadId, - #[serde(rename = "mailboxIds")] MailboxIds, - #[serde(rename = "keywords")] Keywords, - #[serde(rename = "size")] Size, - #[serde(rename = "receivedAt")] ReceivedAt, - #[serde(rename = "messageId")] MessageId, - #[serde(rename = "inReplyTo")] InReplyTo, - #[serde(rename = "references")] References, - #[serde(rename = "sender")] Sender, - #[serde(rename = "from")] From, - #[serde(rename = "to")] To, - #[serde(rename = "cc")] Cc, - #[serde(rename = "bcc")] Bcc, - #[serde(rename = "replyTo")] ReplyTo, - #[serde(rename = "subject")] Subject, - #[serde(rename = "sentAt")] SentAt, - #[serde(rename = "bodyStructure")] BodyStructure, - #[serde(rename = "bodyValues")] BodyValues, - #[serde(rename = "textBody")] TextBody, - #[serde(rename = "htmlBody")] HtmlBody, - #[serde(rename = "attachments")] Attachments, - #[serde(rename = "hasAttachment")] HasAttachment, - #[serde(rename = "preview")] Preview, + Header(Header), } #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum BodyProperty { - #[serde(rename = "partId")] - PartId, - #[serde(rename = "blobId")] - BlobId, - #[serde(rename = "size")] - Size, - #[serde(rename = "headers")] - Headers, - #[serde(rename = "name")] - Name, - #[serde(rename = "type")] - Type, - #[serde(rename = "charset")] - Charset, - #[serde(rename = "disposition")] - Disposition, - #[serde(rename = "cid")] - Cid, - #[serde(rename = "language")] - Language, - #[serde(rename = "location")] - Location, - #[serde(rename = "subParts")] - SubParts, +#[serde(untagged)] +pub enum HeaderValue { + AsDate(DateTime), + AsDateAll(Vec>), + AsText(String), + AsTextAll(Vec), + AsTextListAll(Vec>), + AsAddressesAll(Vec>), + AsAddresses(Vec), + AsGroupedAddressesAll(Vec>), + AsGroupedAddresses(Vec), +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone)] +pub struct Header { + pub name: String, + pub form: HeaderForm, + pub all: bool, +} + +#[derive(PartialEq, Eq, Hash, Debug, Clone, PartialOrd, Ord)] +pub enum HeaderForm { + Raw, + Text, + Addresses, + GroupedAddresses, + MessageIds, + Date, + URLs, +} + +impl Property { + fn parse(value: &str) -> Option { + match value { + "id" => Some(Property::Id), + "blobId" => Some(Property::BlobId), + "threadId" => Some(Property::ThreadId), + "mailboxIds" => Some(Property::MailboxIds), + "keywords" => Some(Property::Keywords), + "size" => Some(Property::Size), + "receivedAt" => Some(Property::ReceivedAt), + "messageId" => Some(Property::MessageId), + "inReplyTo" => Some(Property::InReplyTo), + "references" => Some(Property::References), + "sender" => Some(Property::Sender), + "from" => Some(Property::From), + "to" => Some(Property::To), + "cc" => Some(Property::Cc), + "bcc" => Some(Property::Bcc), + "replyTo" => Some(Property::ReplyTo), + "subject" => Some(Property::Subject), + "sentAt" => Some(Property::SentAt), + "hasAttachment" => Some(Property::HasAttachment), + "preview" => Some(Property::Preview), + "bodyValues" => Some(Property::BodyValues), + "textBody" => Some(Property::TextBody), + "htmlBody" => Some(Property::HtmlBody), + "attachments" => Some(Property::Attachments), + "bodyStructure" => Some(Property::BodyStructure), + _ if value.starts_with("header:") => Some(Property::Header(Header::parse(value)?)), + _ => None, + } + } } impl Display for Property { @@ -350,10 +382,303 @@ impl Display for Property { Property::Attachments => write!(f, "attachments"), Property::HasAttachment => write!(f, "hasAttachment"), Property::Preview => write!(f, "preview"), + Property::Header(header) => header.fmt(f), } } } +impl Serialize for Property { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +struct PropertyVisitor; + +impl<'de> Visitor<'de> for PropertyVisitor { + type Value = Property; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid JMAP e-mail property") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Property::parse(v).ok_or_else(|| { + serde::de::Error::custom(format!("Failed to parse JMAP property '{}'", v)) + }) + } +} + +impl<'de> Deserialize<'de> for Property { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(PropertyVisitor) + } +} + +impl Serialize for Header { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +struct HeaderVisitor; + +impl<'de> Visitor<'de> for HeaderVisitor { + type Value = Header; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid JMAP header") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Header::parse(v) + .ok_or_else(|| serde::de::Error::custom(format!("Failed to parse JMAP header '{}'", v))) + } +} + +impl<'de> Deserialize<'de> for Header { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(HeaderVisitor) + } +} + +impl HeaderForm { + pub fn parse(value: &str) -> Option { + match value { + "asText" => Some(HeaderForm::Text), + "asAddresses" => Some(HeaderForm::Addresses), + "asGroupedAddresses" => Some(HeaderForm::GroupedAddresses), + "asMessageIds" => Some(HeaderForm::MessageIds), + "asDate" => Some(HeaderForm::Date), + "asURLs" => Some(HeaderForm::URLs), + _ => None, + } + } +} + +impl Header { + pub fn as_raw(name: impl Into, all: bool) -> Header { + Header { + name: name.into(), + form: HeaderForm::Raw, + all, + } + } + + pub fn as_text(name: impl Into, all: bool) -> Header { + Header { + name: name.into(), + form: HeaderForm::Text, + all, + } + } + + pub fn as_addresses(name: impl Into, all: bool) -> Header { + Header { + name: name.into(), + form: HeaderForm::Addresses, + all, + } + } + + pub fn as_grouped_addresses(name: impl Into, all: bool) -> Header { + Header { + name: name.into(), + form: HeaderForm::GroupedAddresses, + all, + } + } + + pub fn as_message_ids(name: impl Into, all: bool) -> Header { + Header { + name: name.into(), + form: HeaderForm::MessageIds, + all, + } + } + + pub fn as_date(name: impl Into, all: bool) -> Header { + Header { + name: name.into(), + form: HeaderForm::Date, + all, + } + } + + pub fn as_urls(name: impl Into, all: bool) -> Header { + Header { + name: name.into(), + form: HeaderForm::URLs, + all, + } + } + + pub fn parse(value: &str) -> Option
{ + let mut all = false; + let mut form = HeaderForm::Raw; + let mut header = None; + for (pos, part) in value.split(':').enumerate() { + match pos { + 0 if part == "header" => (), + 1 => { + header = part.into(); + } + 2 | 3 if part == "all" => all = true, + 2 => { + form = HeaderForm::parse(part)?; + } + _ => return None, + } + } + Header { + name: header?.to_string(), + form, + all, + } + .into() + } +} + +impl Display for Header { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "header:")?; + self.name.fmt(f)?; + self.form.fmt(f)?; + if self.all { + write!(f, ":all") + } else { + Ok(()) + } + } +} + +impl Display for HeaderForm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HeaderForm::Raw => Ok(()), + HeaderForm::Text => write!(f, ":asText"), + HeaderForm::Addresses => write!(f, ":asAddresses"), + HeaderForm::GroupedAddresses => write!(f, ":asGroupedAddresses"), + HeaderForm::MessageIds => write!(f, ":asMessageIds"), + HeaderForm::Date => write!(f, ":asDate"), + HeaderForm::URLs => write!(f, ":asURLs"), + } + } +} + +#[derive(Debug, Clone)] +pub enum BodyProperty { + PartId, + BlobId, + Size, + Headers, + Name, + Type, + Charset, + Disposition, + Cid, + Language, + Location, + SubParts, + Header(Header), +} + +impl BodyProperty { + fn parse(value: &str) -> Option { + match value { + "partId" => Some(BodyProperty::PartId), + "blobId" => Some(BodyProperty::BlobId), + "size" => Some(BodyProperty::Size), + "name" => Some(BodyProperty::Name), + "type" => Some(BodyProperty::Type), + "charset" => Some(BodyProperty::Charset), + "headers" => Some(BodyProperty::Headers), + "disposition" => Some(BodyProperty::Disposition), + "cid" => Some(BodyProperty::Cid), + "language" => Some(BodyProperty::Language), + "location" => Some(BodyProperty::Location), + "subParts" => Some(BodyProperty::SubParts), + _ if value.starts_with("header:") => Some(BodyProperty::Header(Header::parse(value)?)), + _ => None, + } + } +} + +impl Display for BodyProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BodyProperty::PartId => write!(f, "partId"), + BodyProperty::BlobId => write!(f, "blobId"), + BodyProperty::Size => write!(f, "size"), + BodyProperty::Name => write!(f, "name"), + BodyProperty::Type => write!(f, "type"), + BodyProperty::Charset => write!(f, "charset"), + BodyProperty::Header(header) => header.fmt(f), + BodyProperty::Headers => write!(f, "headers"), + BodyProperty::Disposition => write!(f, "disposition"), + BodyProperty::Cid => write!(f, "cid"), + BodyProperty::Language => write!(f, "language"), + BodyProperty::Location => write!(f, "location"), + BodyProperty::SubParts => write!(f, "subParts"), + } + } +} + +impl Serialize for BodyProperty { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +struct BodyPropertyVisitor; + +impl<'de> Visitor<'de> for BodyPropertyVisitor { + type Value = BodyProperty; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid JMAP body property") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + BodyProperty::parse(v).ok_or_else(|| { + serde::de::Error::custom(format!("Failed to parse JMAP body property '{}'", v)) + }) + } +} + +impl<'de> Deserialize<'de> for BodyProperty { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(BodyPropertyVisitor) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MailCapabilities { #[serde(rename = "maxMailboxesPerEmail")] @@ -387,7 +712,8 @@ pub struct SubmissionCapabilities { #[derive(Debug, Clone, Serialize, Default)] pub struct QueryArguments { #[serde(rename = "collapseThreads")] - collapse_threads: bool, + #[serde(skip_serializing_if = "Option::is_none")] + collapse_threads: Option, } #[derive(Debug, Clone, Serialize, Default)] @@ -395,19 +721,27 @@ pub struct GetArguments { #[serde(rename = "bodyProperties")] #[serde(skip_serializing_if = "Option::is_none")] body_properties: Option>, + #[serde(rename = "fetchTextBodyValues")] - fetch_text_body_values: bool, + #[serde(skip_serializing_if = "Option::is_none")] + fetch_text_body_values: Option, + #[serde(rename = "fetchHTMLBodyValues")] - fetch_html_body_values: bool, + #[serde(skip_serializing_if = "Option::is_none")] + fetch_html_body_values: Option, + #[serde(rename = "fetchAllBodyValues")] - fetch_all_body_values: bool, + #[serde(skip_serializing_if = "Option::is_none")] + fetch_all_body_values: Option, + #[serde(rename = "maxBodyValueBytes")] - max_body_value_bytes: usize, + #[serde(skip_serializing_if = "Option::is_none")] + max_body_value_bytes: Option, } impl QueryArguments { pub fn collapse_threads(&mut self, collapse_threads: bool) { - self.collapse_threads = collapse_threads; + self.collapse_threads = collapse_threads.into(); } } @@ -421,22 +755,22 @@ impl GetArguments { } pub fn fetch_text_body_values(&mut self, fetch_text_body_values: bool) -> &mut Self { - self.fetch_text_body_values = fetch_text_body_values; + self.fetch_text_body_values = fetch_text_body_values.into(); self } pub fn fetch_html_body_values(&mut self, fetch_html_body_values: bool) -> &mut Self { - self.fetch_html_body_values = fetch_html_body_values; + self.fetch_html_body_values = fetch_html_body_values.into(); self } pub fn fetch_all_body_values(&mut self, fetch_all_body_values: bool) -> &mut Self { - self.fetch_all_body_values = fetch_all_body_values; + self.fetch_all_body_values = fetch_all_body_values.into(); self } pub fn max_body_value_bytes(&mut self, max_body_value_bytes: usize) -> &mut Self { - self.max_body_value_bytes = max_body_value_bytes; + self.max_body_value_bytes = max_body_value_bytes.into(); self } } @@ -476,3 +810,150 @@ impl SubmissionCapabilities { &self.submission_extensions } } + +#[cfg(feature = "debug")] +use std::collections::BTreeMap; + +#[cfg(feature = "debug")] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TestEmail { + #[serde(rename = "mailboxIds")] + pub mailbox_ids: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub keywords: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + #[serde(rename = "receivedAt")] + #[serde(skip_serializing_if = "Option::is_none")] + pub received_at: Option>, + + #[serde(rename = "messageId")] + #[serde(skip_serializing_if = "Option::is_none")] + pub message_id: Option>, + + #[serde(rename = "inReplyTo")] + #[serde(skip_serializing_if = "Option::is_none")] + pub in_reply_to: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub references: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub sender: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub cc: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub bcc: Option>, + + #[serde(rename = "replyTo")] + #[serde(skip_serializing_if = "Option::is_none")] + pub reply_to: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option, + + #[serde(rename = "sentAt")] + #[serde(skip_serializing_if = "Option::is_none")] + pub sent_at: Option>, + + #[serde(rename = "bodyStructure")] + #[serde(skip_serializing_if = "Option::is_none")] + pub body_structure: Option>, + + #[serde(rename = "bodyValues")] + #[serde(skip_serializing_if = "Option::is_none")] + pub body_values: Option>, + + #[serde(rename = "textBody")] + #[serde(skip_serializing_if = "Option::is_none")] + pub text_body: Option>, + + #[serde(rename = "htmlBody")] + #[serde(skip_serializing_if = "Option::is_none")] + pub html_body: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub attachments: Option>, + + #[serde(rename = "hasAttachment")] + #[serde(skip_serializing_if = "Option::is_none")] + pub has_attachment: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub preview: Option, + + #[serde(flatten)] + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub headers: BTreeMap>, +} + +#[cfg(feature = "debug")] +impl From for TestEmail { + fn from(email: Email) -> Self { + TestEmail { + mailbox_ids: email.mailbox_ids.map(|ids| ids.into_iter().collect()), + keywords: email + .keywords + .map(|keywords| keywords.into_iter().collect()), + size: email.size, + received_at: email.received_at, + message_id: email.message_id, + in_reply_to: email.in_reply_to, + references: email.references, + sender: email.sender, + from: email.from, + to: email.to, + cc: email.cc, + bcc: email.bcc, + reply_to: email.reply_to, + subject: email.subject, + sent_at: email.sent_at, + body_structure: email.body_structure.map(|b| b.into_sorted_part().into()), + body_values: email + .body_values + .map(|body_values| body_values.into_iter().collect()), + text_body: email + .text_body + .map(|parts| parts.into_iter().map(|b| b.into_sorted_part()).collect()), + html_body: email + .html_body + .map(|parts| parts.into_iter().map(|b| b.into_sorted_part()).collect()), + attachments: email + .attachments + .map(|parts| parts.into_iter().map(|b| b.into_sorted_part()).collect()), + has_attachment: email.has_attachment, + preview: email.preview, + headers: email.headers.into_iter().collect(), + } + } +} + +#[cfg(feature = "debug")] +impl EmailBodyPart { + pub fn sort_headers(&mut self) { + if let Some(headers) = self.headers.as_mut() { + headers.sort_unstable_by_key(|h| (h.name.clone(), h.value.clone())); + } + if let Some(subparts) = self.sub_parts.as_mut() { + for sub_part in subparts { + sub_part.sort_headers(); + } + } + } + + pub fn into_sorted_part(mut self) -> Self { + self.sort_headers(); + self + } +} diff --git a/src/email/parse.rs b/src/email/parse.rs index 6ec22a3..94d34ed 100644 --- a/src/email/parse.rs +++ b/src/email/parse.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use crate::Error; + use super::{BodyProperty, Email, Property}; #[derive(Debug, Clone, Serialize)] @@ -13,22 +15,28 @@ pub struct EmailParseRequest { blob_ids: Vec, #[serde(rename = "properties")] - properties: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + properties: Option>, #[serde(rename = "bodyProperties")] - body_properties: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + body_properties: Option>, #[serde(rename = "fetchTextBodyValues")] - fetch_text_body_values: bool, + #[serde(skip_serializing_if = "Option::is_none")] + fetch_text_body_values: Option, #[serde(rename = "fetchHTMLBodyValues")] - fetch_html_body_values: bool, + #[serde(skip_serializing_if = "Option::is_none")] + fetch_html_body_values: Option, #[serde(rename = "fetchAllBodyValues")] - fetch_all_body_values: bool, + #[serde(skip_serializing_if = "Option::is_none")] + fetch_all_body_values: Option, #[serde(rename = "maxBodyValueBytes")] - max_body_value_bytes: usize, + #[serde(skip_serializing_if = "Option::is_none")] + max_body_value_bytes: Option, } #[derive(Debug, Clone, Deserialize)] @@ -51,12 +59,12 @@ impl EmailParseRequest { EmailParseRequest { account_id, blob_ids: Vec::new(), - properties: Vec::new(), - body_properties: Vec::new(), - fetch_text_body_values: false, - fetch_html_body_values: false, - fetch_all_body_values: false, - max_body_value_bytes: 0, + properties: None, + body_properties: None, + fetch_text_body_values: None, + fetch_html_body_values: None, + fetch_all_body_values: None, + max_body_value_bytes: None, } } @@ -70,7 +78,7 @@ impl EmailParseRequest { } pub fn properties(&mut self, properties: impl IntoIterator) -> &mut Self { - self.properties = properties.into_iter().collect(); + self.properties = Some(properties.into_iter().collect()); self } @@ -78,27 +86,27 @@ impl EmailParseRequest { &mut self, body_properties: impl IntoIterator, ) -> &mut Self { - self.body_properties = body_properties.into_iter().collect(); + self.body_properties = Some(body_properties.into_iter().collect()); self } pub fn fetch_text_body_values(&mut self, fetch_text_body_values: bool) -> &mut Self { - self.fetch_text_body_values = fetch_text_body_values; + self.fetch_text_body_values = fetch_text_body_values.into(); self } pub fn fetch_html_body_values(&mut self, fetch_html_body_values: bool) -> &mut Self { - self.fetch_html_body_values = fetch_html_body_values; + self.fetch_html_body_values = fetch_html_body_values.into(); self } pub fn fetch_all_body_values(&mut self, fetch_all_body_values: bool) -> &mut Self { - self.fetch_all_body_values = fetch_all_body_values; + self.fetch_all_body_values = fetch_all_body_values.into(); self } pub fn max_body_value_bytes(&mut self, max_body_value_bytes: usize) -> &mut Self { - self.max_body_value_bytes = max_body_value_bytes; + self.max_body_value_bytes = max_body_value_bytes.into(); self } } @@ -108,12 +116,26 @@ impl EmailParseResponse { &self.account_id } - pub fn parsed(&self) -> Option> { - self.parsed.as_ref().map(|map| map.keys()) + pub fn parsed(&mut self, blob_id: &str) -> crate::Result { + if let Some(result) = self.parsed.as_mut().and_then(|r| r.remove(blob_id)) { + Ok(result) + } else if self + .not_parsable + .as_ref() + .map(|np| np.iter().any(|id| id == blob_id)) + .unwrap_or(false) + { + Err(Error::Internal(format!( + "blobId {} is not parsable.", + blob_id + ))) + } else { + Err(Error::Internal(format!("blobId {} not found.", blob_id))) + } } - pub fn parsed_details(&self, id: &str) -> Option<&Email> { - self.parsed.as_ref().and_then(|map| map.get(id)) + pub fn parsed_list(&self) -> Option> { + self.parsed.as_ref().map(|map| map.iter()) } pub fn not_parsable(&self) -> Option<&[String]> { diff --git a/src/email/set.rs b/src/email/set.rs index b98c2ec..f3509c8 100644 --- a/src/email/set.rs +++ b/src/email/set.rs @@ -9,7 +9,8 @@ use crate::{ }; use super::{ - Email, EmailAddress, EmailAddressGroup, EmailBodyPart, EmailBodyValue, EmailHeader, Field, + Email, EmailAddress, EmailAddressGroup, EmailBodyPart, EmailBodyValue, EmailHeader, Header, + HeaderValue, }; impl Email { @@ -31,10 +32,7 @@ impl Email { pub fn mailbox_id(&mut self, mailbox_id: &str, set: bool) -> &mut Self { self.mailbox_ids = None; - self.others.insert( - format!("mailboxIds/{}", mailbox_id), - Field::Bool(set).into(), - ); + self.patch.insert(format!("mailboxIds/{}", mailbox_id), set); self } @@ -49,8 +47,7 @@ impl Email { pub fn keyword(&mut self, keyword: &str, set: bool) -> &mut Self { self.keywords = None; - self.others - .insert(format!("keywords/{}", keyword), Field::Bool(set).into()); + self.patch.insert(format!("keywords/{}", keyword), set); self } @@ -174,8 +171,8 @@ impl Email { self } - pub fn header(&mut self, header: String, value: impl Into) -> &mut Self { - self.others.insert(header, Some(value.into())); + pub fn header(&mut self, header: Header, value: impl Into) -> &mut Self { + self.headers.insert(header, Some(value.into())); self } } @@ -211,7 +208,8 @@ impl Create for Email { attachments: Default::default(), has_attachment: Default::default(), preview: Default::default(), - others: Default::default(), + headers: Default::default(), + patch: Default::default(), } } @@ -235,6 +233,7 @@ impl EmailBodyPart { language: None, location: None, sub_parts: None, + header: None, _state: Default::default(), } } diff --git a/src/mailbox/get.rs b/src/mailbox/get.rs index 5a7c3e2..84f2611 100644 --- a/src/mailbox/get.rs +++ b/src/mailbox/get.rs @@ -7,6 +7,10 @@ impl Mailbox { self.id.as_ref().unwrap() } + pub fn unwrap_id(self) -> String { + self.id.unwrap() + } + pub fn name(&self) -> &str { self.name.as_ref().unwrap() } diff --git a/src/thread/helpers.rs b/src/thread/helpers.rs new file mode 100644 index 0000000..5752638 --- /dev/null +++ b/src/thread/helpers.rs @@ -0,0 +1,14 @@ +use crate::{client::Client, core::response::ThreadGetResponse}; + +use super::Thread; + +impl Client { + pub async fn thread_get(&mut self, id: &str) -> crate::Result> { + let mut request = self.build(); + request.get_thread().ids([id]); + request + .send_single::() + .await + .map(|mut r| r.unwrap_list().pop()) + } +} diff --git a/src/thread/mod.rs b/src/thread/mod.rs index 25dc161..3e94a41 100644 --- a/src/thread/mod.rs +++ b/src/thread/mod.rs @@ -1,4 +1,5 @@ pub mod get; +pub mod helpers; use serde::{Deserialize, Serialize};