Improvements and testing.

main
Mauro D 2022-05-13 15:38:26 +00:00
parent a23d97e3a4
commit e24039ab12
13 changed files with 727 additions and 146 deletions

View File

@ -18,7 +18,8 @@ chrono = { version = "0.4", features = ["serde"]}
reqwest = "0.11"
base64 = "0.13"
#[dev-dependencies]
[features]
debug = []
[profile.bench]
debug = true

View File

@ -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<Vec<u8>> {
pub async fn download(&self, blob_id: &str) -> crate::Result<Vec<u8>> {
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(),
);

View File

@ -23,10 +23,10 @@ pub struct UploadResponse {
impl Client {
pub async fn upload(
&self,
account_id: &str,
content_type: Option<&str>,
blob: Vec<u8>,
content_type: Option<&str>,
) -> crate::Result<UploadResponse> {
let account_id = self.default_account_id();
let mut upload_url =
String::with_capacity(self.session().upload_url().len() + account_id.len());

View File

@ -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<String>) {
pub fn set_default_account_id(&mut self, defaul_account_id: impl Into<String>) -> &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::<EmailBodyPart>(
br#"{
"partId": "0",
"header:X-Custom-Header": "123",
"type": "text/html",
"charset": "us-ascii",
"size": 175
}"#
)
.unwrap()
);
/*let coco = request
.send()

View File

@ -14,21 +14,24 @@ pub struct QueryRequest<F, S, A: Default> {
sort: Option<Vec<Comparator<S>>>,
#[serde(rename = "position")]
position: i32,
#[serde(skip_serializing_if = "Option::is_none")]
position: Option<i32>,
#[serde(rename = "anchor")]
#[serde(skip_serializing_if = "Option::is_none")]
anchor: Option<String>,
#[serde(rename = "anchorOffset")]
anchor_offset: i32,
#[serde(skip_serializing_if = "Option::is_none")]
anchor_offset: Option<i32>,
#[serde(rename = "limit")]
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<usize>,
#[serde(rename = "calculateTotal")]
calculate_total: bool,
#[serde(skip_serializing_if = "Option::is_none")]
calculate_total: Option<bool>,
#[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<bool>,
#[serde(rename = "position")]
position: i32,
@ -99,11 +102,11 @@ impl<F, S, A: Default> QueryRequest<F, S, A> {
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<F, S, A: Default> QueryRequest<F, S, A> {
}
pub fn position(&mut self, position: i32) -> &mut Self {
self.position = position;
self.position = position.into();
self
}
@ -134,7 +137,7 @@ impl<F, S, A: Default> QueryRequest<F, S, A> {
}
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)
}
}

View File

@ -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<Get> {
@ -109,12 +110,17 @@ impl Email<Get> {
*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<Get> {
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()
}

View File

@ -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<T, U>(
@ -20,10 +22,7 @@ impl Client {
T: IntoIterator<Item = U>,
U: Into<String>,
{
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<Vec<Property>>,
properties: Option<impl IntoIterator<Item = Property>>,
) -> crate::Result<Option<Email>> {
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::<EmailGetResponse>()
@ -135,4 +134,33 @@ impl Client {
}
request.send_single::<QueryResponse>().await
}
pub async fn email_parse(
&mut self,
blob_id: &str,
properties: Option<impl IntoIterator<Item = Property>>,
body_properties: Option<impl IntoIterator<Item = BodyProperty>>,
max_body_value_bytes: Option<usize>,
) -> crate::Result<Email> {
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::<EmailParseResponse>()
.await
.and_then(|mut r| r.parsed(blob_id))
}
}

View File

@ -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<State = Get> {
#[serde(skip_serializing_if = "Option::is_none")]
received_at: Option<DateTime<Utc>>,
#[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<Vec<String>>,
#[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<Vec<String>>,
#[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<Vec<String>>,
#[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<Vec<EmailAddress>>,
#[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<Vec<EmailAddress>>,
#[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<Vec<EmailAddress>>,
#[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<Vec<EmailAddress>>,
#[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<Vec<EmailAddress>>,
#[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<Vec<EmailAddress>>,
#[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<String>,
#[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<DateTime<Utc>>,
@ -130,7 +150,12 @@ pub struct Email<State = Get> {
#[serde(flatten)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
others: HashMap<String, Option<Field>>,
headers: HashMap<Header, Option<HeaderValue>>,
#[serde(flatten)]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
patch: HashMap<String, bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -185,6 +210,10 @@ pub struct EmailBodyPart<State = Get> {
#[serde(rename = "subParts")]
#[serde(skip_serializing_if = "Option::is_none")]
sub_parts: Option<Vec<EmailBodyPart>>,
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
header: Option<HashMap<Header, HeaderValue>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -202,17 +231,6 @@ pub struct EmailBodyValue<State = Get> {
is_truncated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Field {
Text(String),
TextList(Vec<String>),
Date(DateTime<Utc>),
Addresses(Vec<EmailAddress>),
GroupedAddresses(Vec<EmailAddressGroup>),
Bool(bool),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailAddress<State = Get> {
#[serde(skip)]
@ -240,86 +258,100 @@ pub struct EmailHeader<State = Get> {
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<Utc>),
AsDateAll(Vec<DateTime<Utc>>),
AsText(String),
AsTextAll(Vec<String>),
AsTextListAll(Vec<Vec<String>>),
AsAddressesAll(Vec<Vec<EmailAddress>>),
AsAddresses(Vec<EmailAddress>),
AsGroupedAddressesAll(Vec<Vec<EmailAddressGroup>>),
AsGroupedAddresses(Vec<EmailAddressGroup>),
}
#[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<Self> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(PropertyVisitor)
}
}
impl Serialize for Header {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(HeaderVisitor)
}
}
impl HeaderForm {
pub fn parse(value: &str) -> Option<HeaderForm> {
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<String>, all: bool) -> Header {
Header {
name: name.into(),
form: HeaderForm::Raw,
all,
}
}
pub fn as_text(name: impl Into<String>, all: bool) -> Header {
Header {
name: name.into(),
form: HeaderForm::Text,
all,
}
}
pub fn as_addresses(name: impl Into<String>, all: bool) -> Header {
Header {
name: name.into(),
form: HeaderForm::Addresses,
all,
}
}
pub fn as_grouped_addresses(name: impl Into<String>, all: bool) -> Header {
Header {
name: name.into(),
form: HeaderForm::GroupedAddresses,
all,
}
}
pub fn as_message_ids(name: impl Into<String>, all: bool) -> Header {
Header {
name: name.into(),
form: HeaderForm::MessageIds,
all,
}
}
pub fn as_date(name: impl Into<String>, all: bool) -> Header {
Header {
name: name.into(),
form: HeaderForm::Date,
all,
}
}
pub fn as_urls(name: impl Into<String>, all: bool) -> Header {
Header {
name: name.into(),
form: HeaderForm::URLs,
all,
}
}
pub fn parse(value: &str) -> Option<Header> {
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<BodyProperty> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<bool>,
}
#[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<Vec<BodyProperty>>,
#[serde(rename = "fetchTextBodyValues")]
fetch_text_body_values: bool,
#[serde(skip_serializing_if = "Option::is_none")]
fetch_text_body_values: Option<bool>,
#[serde(rename = "fetchHTMLBodyValues")]
fetch_html_body_values: bool,
#[serde(skip_serializing_if = "Option::is_none")]
fetch_html_body_values: Option<bool>,
#[serde(rename = "fetchAllBodyValues")]
fetch_all_body_values: bool,
#[serde(skip_serializing_if = "Option::is_none")]
fetch_all_body_values: Option<bool>,
#[serde(rename = "maxBodyValueBytes")]
max_body_value_bytes: usize,
#[serde(skip_serializing_if = "Option::is_none")]
max_body_value_bytes: Option<usize>,
}
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<BTreeMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keywords: Option<BTreeMap<String, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<usize>,
#[serde(rename = "receivedAt")]
#[serde(skip_serializing_if = "Option::is_none")]
pub received_at: Option<DateTime<Utc>>,
#[serde(rename = "messageId")]
#[serde(skip_serializing_if = "Option::is_none")]
pub message_id: Option<Vec<String>>,
#[serde(rename = "inReplyTo")]
#[serde(skip_serializing_if = "Option::is_none")]
pub in_reply_to: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub references: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender: Option<Vec<EmailAddress>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<Vec<EmailAddress>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Vec<EmailAddress>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<EmailAddress>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bcc: Option<Vec<EmailAddress>>,
#[serde(rename = "replyTo")]
#[serde(skip_serializing_if = "Option::is_none")]
pub reply_to: Option<Vec<EmailAddress>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(rename = "sentAt")]
#[serde(skip_serializing_if = "Option::is_none")]
pub sent_at: Option<DateTime<Utc>>,
#[serde(rename = "bodyStructure")]
#[serde(skip_serializing_if = "Option::is_none")]
pub body_structure: Option<Box<EmailBodyPart>>,
#[serde(rename = "bodyValues")]
#[serde(skip_serializing_if = "Option::is_none")]
pub body_values: Option<BTreeMap<String, EmailBodyValue>>,
#[serde(rename = "textBody")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_body: Option<Vec<EmailBodyPart>>,
#[serde(rename = "htmlBody")]
#[serde(skip_serializing_if = "Option::is_none")]
pub html_body: Option<Vec<EmailBodyPart>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<EmailBodyPart>>,
#[serde(rename = "hasAttachment")]
#[serde(skip_serializing_if = "Option::is_none")]
pub has_attachment: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview: Option<String>,
#[serde(flatten)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub headers: BTreeMap<Header, Option<HeaderValue>>,
}
#[cfg(feature = "debug")]
impl From<Email> 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
}
}

View File

@ -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<String>,
#[serde(rename = "properties")]
properties: Vec<Property>,
#[serde(skip_serializing_if = "Option::is_none")]
properties: Option<Vec<Property>>,
#[serde(rename = "bodyProperties")]
body_properties: Vec<BodyProperty>,
#[serde(skip_serializing_if = "Option::is_none")]
body_properties: Option<Vec<BodyProperty>>,
#[serde(rename = "fetchTextBodyValues")]
fetch_text_body_values: bool,
#[serde(skip_serializing_if = "Option::is_none")]
fetch_text_body_values: Option<bool>,
#[serde(rename = "fetchHTMLBodyValues")]
fetch_html_body_values: bool,
#[serde(skip_serializing_if = "Option::is_none")]
fetch_html_body_values: Option<bool>,
#[serde(rename = "fetchAllBodyValues")]
fetch_all_body_values: bool,
#[serde(skip_serializing_if = "Option::is_none")]
fetch_all_body_values: Option<bool>,
#[serde(rename = "maxBodyValueBytes")]
max_body_value_bytes: usize,
#[serde(skip_serializing_if = "Option::is_none")]
max_body_value_bytes: Option<usize>,
}
#[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<Item = Property>) -> &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<Item = BodyProperty>,
) -> &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<impl Iterator<Item = &String>> {
self.parsed.as_ref().map(|map| map.keys())
pub fn parsed(&mut self, blob_id: &str) -> crate::Result<Email> {
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<impl Iterator<Item = (&String, &Email)>> {
self.parsed.as_ref().map(|map| map.iter())
}
pub fn not_parsable(&self) -> Option<&[String]> {

View File

@ -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<Set> {
@ -31,10 +32,7 @@ impl Email<Set> {
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<Set> {
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<Set> {
self
}
pub fn header(&mut self, header: String, value: impl Into<Field>) -> &mut Self {
self.others.insert(header, Some(value.into()));
pub fn header(&mut self, header: Header, value: impl Into<HeaderValue>) -> &mut Self {
self.headers.insert(header, Some(value.into()));
self
}
}
@ -211,7 +208,8 @@ impl Create for Email<Set> {
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(),
}
}

View File

@ -7,6 +7,10 @@ impl Mailbox<Get> {
self.id.as_ref().unwrap()
}
pub fn unwrap_id(self) -> String {
self.id.unwrap()
}
pub fn name(&self) -> &str {
self.name.as_ref().unwrap()
}

14
src/thread/helpers.rs Normal file
View File

@ -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<Option<Thread>> {
let mut request = self.build();
request.get_thread().ids([id]);
request
.send_single::<ThreadGetResponse>()
.await
.map(|mut r| r.unwrap_list().pop())
}
}

View File

@ -1,4 +1,5 @@
pub mod get;
pub mod helpers;
use serde::{Deserialize, Serialize};