From d0aef5d5a28d53be16e13685a67b82e6fa1e3f1f Mon Sep 17 00:00:00 2001 From: mdecimus Date: Wed, 1 Oct 2025 17:21:56 +0200 Subject: [PATCH] JMAP for Contacts implementation (part 1) --- CHANGELOG.md | 4 +- Cargo.lock | 3 +- crates/directory/src/core/mod.rs | 12 + crates/directory/src/core/principal.rs | 10 + crates/directory/src/lib.rs | 12 + crates/jmap-proto/Cargo.toml | 1 + crates/jmap-proto/src/error/set.rs | 4 + crates/jmap-proto/src/method/mod.rs | 20 +- crates/jmap-proto/src/method/parse.rs | 50 ++- crates/jmap-proto/src/object/addressbook.rs | 333 ++++++++++++++++++ crates/jmap-proto/src/object/blob.rs | 2 + crates/jmap-proto/src/object/contact.rs | 230 ++++++++++++ crates/jmap-proto/src/object/email.rs | 41 +++ .../jmap-proto/src/object/email_submission.rs | 2 + crates/jmap-proto/src/object/identity.rs | 5 +- crates/jmap-proto/src/object/mailbox.rs | 10 +- crates/jmap-proto/src/object/mod.rs | 6 +- crates/jmap-proto/src/object/principal.rs | 2 + .../src/object/push_subscription.rs | 2 + crates/jmap-proto/src/object/quota.rs | 2 + crates/jmap-proto/src/object/sieve.rs | 2 + crates/jmap-proto/src/object/thread.rs | 2 + .../src/object/vacation_response.rs | 2 + crates/jmap-proto/src/references/eval.rs | 12 + crates/jmap-proto/src/references/jsptr.rs | 2 +- crates/jmap-proto/src/references/resolve.rs | 5 + crates/jmap-proto/src/request/capability.rs | 32 ++ crates/jmap-proto/src/request/method.rs | 28 ++ crates/jmap-proto/src/request/mod.rs | 24 +- crates/jmap-proto/src/request/parser.rs | 64 +++- crates/jmap-proto/src/response/mod.rs | 77 +++- crates/jmap/src/addressbook/get.rs | 29 ++ crates/jmap/src/addressbook/mod.rs | 8 + crates/jmap/src/addressbook/set.rs | 32 ++ crates/jmap/src/api/auth.rs | 22 +- crates/jmap/src/api/request.rs | 83 ++++- crates/jmap/src/changes/get.rs | 6 +- crates/jmap/src/changes/query.rs | 28 +- crates/jmap/src/contact/copy.rs | 35 ++ crates/jmap/src/contact/get.rs | 29 ++ crates/jmap/src/contact/mod.rs | 11 + crates/jmap/src/contact/parse.rs | 29 ++ crates/jmap/src/contact/query.rs | 29 ++ crates/jmap/src/contact/set.rs | 32 ++ crates/jmap/src/email/parse.rs | 35 +- crates/jmap/src/lib.rs | 2 + 46 files changed, 1307 insertions(+), 104 deletions(-) create mode 100644 crates/jmap-proto/src/object/addressbook.rs create mode 100644 crates/jmap-proto/src/object/contact.rs create mode 100644 crates/jmap/src/addressbook/get.rs create mode 100644 crates/jmap/src/addressbook/mod.rs create mode 100644 crates/jmap/src/addressbook/set.rs create mode 100644 crates/jmap/src/contact/copy.rs create mode 100644 crates/jmap/src/contact/get.rs create mode 100644 crates/jmap/src/contact/mod.rs create mode 100644 crates/jmap/src/contact/parse.rs create mode 100644 crates/jmap/src/contact/query.rs create mode 100644 crates/jmap/src/contact/set.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ed855f57..5859f9be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,12 @@ If you are upgrading from v0.11.x or v0.12.x, this version includes **breaking c - JMAP: Protocol layer rewrite for zero-copy deserialization and architectural improvements. ## Fixed +- IMAP: Unbounded memory allocation in request parser ([CVE-2025-61600 ](https://github.com/stalwartlabs/stalwart/security/advisories/GHSA-8jqj-qj5p-v5rr)). +- IMAP: Wrong permission checked for GETACL. - JMAP: References to previous method fail when there are no results (#1507). - JMAP: Enforce quota checks on `Blob/copy`. - JMAP: `Mailbox/get` fails without `accountId` argument (#1936). - JMAP: Do not return `invalidProperties` when email update doesn't contain changes (#1139) -- IMAP: Unbounded memory allocation in request parser. -- IMAP: Wrong permission checked for GETACL. - iTIP: Include date properties in `REPLY` (#2102). - OIDC: Do not set `username` field if it is the same as the `email` field. - Telemetry: Fix `calculateMetrics` housekeeper task (#2155). diff --git a/Cargo.lock b/Cargo.lock index 98524794..d43f6733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "calcard" -version = "0.2.0" +version = "0.3.0" dependencies = [ "ahash", "chrono", @@ -3951,6 +3951,7 @@ name = "jmap_proto" version = "0.13.4" dependencies = [ "ahash", + "calcard", "compact_str", "hashify", "jmap-tools", diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs index 76b43698..a7156a0a 100644 --- a/crates/directory/src/core/mod.rs +++ b/crates/directory/src/core/mod.rs @@ -251,6 +251,18 @@ impl Permission { Permission::CalendarSchedulingReceive => { "Receive calendar scheduling requests via e-mail" } + Permission::JmapAddressBookGet => "Retrieve address books via JMAP", + Permission::JmapAddressBookSet => "Create or update address books via JMAP", + Permission::JmapAddressBookChanges => "Track address book changes via JMAP", + Permission::JmapContactCardGet => "Retrieve contact cards via JMAP", + Permission::JmapContactCardChanges => "Track contact card changes via JMAP", + Permission::JmapContactCardQuery => { + "Search for contact cards matching criteria via JMAP" + } + Permission::JmapContactCardQueryChanges => "Track contact card query changes via JMAP", + Permission::JmapContactCardSet => "Create or update contact cards via JMAP", + Permission::JmapContactCardCopy => "Copy contact cards to new locations via JMAP", + Permission::JmapContactCardParse => "Parse contact cards via JMAP", } } } diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index 6320e4ef..ef015790 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -1465,6 +1465,16 @@ impl Permission { | Permission::CalendarAlarms | Permission::CalendarSchedulingSend | Permission::CalendarSchedulingReceive + | Permission::JmapAddressBookGet + | Permission::JmapAddressBookSet + | Permission::JmapAddressBookChanges + | Permission::JmapContactCardGet + | Permission::JmapContactCardChanges + | Permission::JmapContactCardQuery + | Permission::JmapContactCardQueryChanges + | Permission::JmapContactCardSet + | Permission::JmapContactCardCopy + | Permission::JmapContactCardParse ) } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index cd3b6452..2db6d69e 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -379,6 +379,18 @@ pub enum Permission { CalendarAlarms, CalendarSchedulingSend, CalendarSchedulingReceive, + + JmapAddressBookGet, + JmapAddressBookSet, + JmapAddressBookChanges, + + JmapContactCardGet, + JmapContactCardChanges, + JmapContactCardQuery, + JmapContactCardQueryChanges, + JmapContactCardSet, + JmapContactCardCopy, + JmapContactCardParse, // WARNING: add new ids at the end (TODO: use static ids) } diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml index d71081fc..ddcb1276 100644 --- a/crates/jmap-proto/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -10,6 +10,7 @@ utils = { path = "../utils" } types = { path = "../types" } trc = { path = "../trc" } jmap-tools = { version = "0.1" } +calcard = { path = "/Users/me/code/calcard" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } serde = { version = "1.0", features = ["derive"]} ahash = { version = "0.8.2", features = ["serde"] } diff --git a/crates/jmap-proto/src/error/set.rs b/crates/jmap-proto/src/error/set.rs index e510eb9b..b3fdef56 100644 --- a/crates/jmap-proto/src/error/set.rs +++ b/crates/jmap-proto/src/error/set.rs @@ -9,6 +9,7 @@ use std::borrow::Cow; use types::id::Id; #[derive(Debug, Clone, serde::Serialize)] +#[serde(bound(serialize = "InvalidProperty

: serde::Serialize"))] pub struct SetError { #[serde(rename = "type")] pub type_: SetErrorType, @@ -82,6 +83,8 @@ pub enum SetErrorType { InvalidScript, #[serde(rename = "scriptIsActive")] ScriptIsActive, + #[serde(rename = "addressBookHasContents")] + AddressBookHasContents, } impl SetErrorType { @@ -112,6 +115,7 @@ impl SetErrorType { SetErrorType::AlreadyExists => "alreadyExists", SetErrorType::InvalidScript => "invalidScript", SetErrorType::ScriptIsActive => "scriptIsActive", + SetErrorType::AddressBookHasContents => "addressBookHasContents", } } } diff --git a/crates/jmap-proto/src/method/mod.rs b/crates/jmap-proto/src/method/mod.rs index a064e9b1..61611ccb 100644 --- a/crates/jmap-proto/src/method/mod.rs +++ b/crates/jmap-proto/src/method/mod.rs @@ -7,7 +7,7 @@ use ahash::AHashMap; use jmap_tools::Property; use serde::{ - Deserialize, Deserializer, + Deserialize, Deserializer, Serialize, Serializer, de::{self, MapAccess, Visitor}, }; use std::{borrow::Cow, fmt, str::FromStr}; @@ -30,19 +30,25 @@ fn ahash_is_empty(map: &AHashMap) -> bool { map.is_empty() } -#[derive(Debug, Clone, serde::Serialize)] -#[serde(transparent)] +#[derive(Debug, Clone)] #[repr(transparent)] -pub struct PropertyWrapper(pub T) -where - T: serde::Serialize + Property; +pub struct PropertyWrapper(pub T); -impl From for PropertyWrapper { +impl From for PropertyWrapper { fn from(value: T) -> Self { Self(value) } } +impl Serialize for PropertyWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.0.to_cow().as_ref()) + } +} + pub(crate) struct JmapDict(pub Vec); struct JmapDictVisitor<'de, T: FromStr> { diff --git a/crates/jmap-proto/src/method/parse.rs b/crates/jmap-proto/src/method/parse.rs index 999f48c9..ce7ac697 100644 --- a/crates/jmap-proto/src/method/parse.rs +++ b/crates/jmap-proto/src/method/parse.rs @@ -5,7 +5,7 @@ */ use crate::{ - object::email::{EmailProperty, EmailValue}, + object::JmapObject, request::{ MaybeInvalid, deserialize::{DeserializeArguments, deserialize_request}, @@ -16,26 +16,22 @@ use serde::{Deserialize, Deserializer}; use types::{blob::BlobId, id::Id}; use utils::map::vec_map::VecMap; -#[derive(Debug, Clone, Default)] -pub struct ParseEmailRequest { +#[derive(Debug, Clone)] +pub struct ParseRequest { pub account_id: Id, pub blob_ids: Vec>, - pub properties: Option>>, - pub body_properties: Option>>, - pub fetch_text_body_values: Option, - pub fetch_html_body_values: Option, - pub fetch_all_body_values: Option, - pub max_body_value_bytes: Option, + pub properties: Option>>, + pub arguments: T::ParseArguments, } #[derive(Debug, Clone, serde::Serialize)] -pub struct ParseEmailResponse { +pub struct ParseResponse { #[serde(rename = "accountId")] pub account_id: Id, #[serde(rename = "parsed")] #[serde(skip_serializing_if = "VecMap::is_empty")] - pub parsed: VecMap>, + pub parsed: VecMap>, #[serde(rename = "notParsable")] #[serde(skip_serializing_if = "Vec::is_empty")] @@ -46,7 +42,7 @@ pub struct ParseEmailResponse { pub not_found: Vec, } -impl<'de> DeserializeArguments<'de> for ParseEmailRequest { +impl<'de, T: JmapObject> DeserializeArguments<'de> for ParseRequest { fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> where A: serde::de::MapAccess<'de>, @@ -61,23 +57,8 @@ impl<'de> DeserializeArguments<'de> for ParseEmailRequest { b"properties" => { self.properties = map.next_value()?; }, - b"bodyProperties" => { - self.body_properties = map.next_value()?; - }, - b"fetchTextBodyValues" => { - self.fetch_text_body_values = map.next_value()?; - }, - b"fetchHTMLBodyValues" => { - self.fetch_html_body_values = map.next_value()?; - }, - b"fetchAllBodyValues" => { - self.fetch_all_body_values = map.next_value()?; - }, - b"maxBodyValueBytes" => { - self.max_body_value_bytes = map.next_value()?; - }, _ => { - let _ = map.next_value::()?; + self.arguments.deserialize_argument(key, map)?; } ); @@ -85,7 +66,7 @@ impl<'de> DeserializeArguments<'de> for ParseEmailRequest { } } -impl<'de> Deserialize<'de> for ParseEmailRequest { +impl<'de, T: JmapObject> Deserialize<'de> for ParseRequest { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -93,3 +74,14 @@ impl<'de> Deserialize<'de> for ParseEmailRequest { deserialize_request(deserializer) } } + +impl Default for ParseRequest { + fn default() -> Self { + Self { + account_id: Id::default(), + blob_ids: Vec::default(), + properties: None, + arguments: T::ParseArguments::default(), + } + } +} diff --git a/crates/jmap-proto/src/object/addressbook.rs b/crates/jmap-proto/src/object/addressbook.rs new file mode 100644 index 00000000..e5c32fa0 --- /dev/null +++ b/crates/jmap-proto/src/object/addressbook.rs @@ -0,0 +1,333 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + object::{ + AnyId, JmapObject, JmapObjectId, JmapRight, JmapSharedObject, MaybeReference, parse_ref, + }, + request::deserialize::DeserializeArguments, +}; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::{acl::Acl, id::Id, special_use::SpecialUse}; + +#[derive(Debug, Clone, Default)] +pub struct AddressBook; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AddressBookProperty { + Id, + Name, + Description, + SortOrder, + IsDefault, + IsSubscribed, + ShareWith, + MyRights, + + // Other + IdValue(Id), + Rights(AddressBookRight), + Pointer(JsonPointer), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AddressBookRight { + MayRead, + MayWrite, + MayShare, + MayDelete, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AddressBookValue { + Id(Id), + IdReference(String), + Role(SpecialUse), +} + +impl Property for AddressBookProperty { + fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option { + let allow_patch = key.is_none(); + if let Some(Key::Property(key)) = key { + match key.patch_or_prop() { + AddressBookProperty::ShareWith => { + Id::from_str(value).ok().map(AddressBookProperty::IdValue) + } + _ => AddressBookProperty::parse(value, allow_patch), + } + } else { + AddressBookProperty::parse(value, allow_patch) + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + AddressBookProperty::Id => "id", + AddressBookProperty::Name => "name", + AddressBookProperty::Description => "description", + AddressBookProperty::SortOrder => "sortOrder", + AddressBookProperty::IsDefault => "isDefault", + AddressBookProperty::IsSubscribed => "isSubscribed", + AddressBookProperty::ShareWith => "shareWith", + AddressBookProperty::MyRights => "myRights", + AddressBookProperty::Rights(addressbook_right) => addressbook_right.as_str(), + AddressBookProperty::Pointer(json_pointer) => return json_pointer.to_string().into(), + AddressBookProperty::IdValue(id) => return id.to_string().into(), + } + .into() + } +} + +impl AddressBookRight { + pub fn as_str(&self) -> &'static str { + match self { + AddressBookRight::MayRead => "mayRead", + AddressBookRight::MayWrite => "mayWrite", + AddressBookRight::MayShare => "mayShare", + AddressBookRight::MayDelete => "mayDelete", + } + } +} + +impl Element for AddressBookValue { + type Property = AddressBookProperty; + + fn try_parse

(key: &Key<'_, Self::Property>, value: &str) -> Option { + if let Key::Property(prop) = key { + match prop.patch_or_prop() { + AddressBookProperty::Id => match parse_ref(value) { + MaybeReference::Value(v) => Some(AddressBookValue::Id(v)), + MaybeReference::Reference(v) => Some(AddressBookValue::IdReference(v)), + MaybeReference::ParseError => None, + }, + _ => None, + } + } else { + None + } + } + + fn to_cow(&self) -> Cow<'static, str> { + match self { + AddressBookValue::Id(id) => id.to_string().into(), + AddressBookValue::IdReference(r) => format!("#{r}").into(), + AddressBookValue::Role(special_use) => special_use.as_str().unwrap_or_default().into(), + } + } +} + +impl AddressBookProperty { + fn parse(value: &str, allow_patch: bool) -> Option { + hashify::tiny_map!(value.as_bytes(), + b"id" => AddressBookProperty::Id, + b"name" => AddressBookProperty::Name, + b"description" => AddressBookProperty::Description, + b"sortOrder" => AddressBookProperty::SortOrder, + b"isDefault" => AddressBookProperty::IsDefault, + b"isSubscribed" => AddressBookProperty::IsSubscribed, + b"shareWith" => AddressBookProperty::ShareWith, + b"myRights" => AddressBookProperty::MyRights, + b"mayRead" => AddressBookProperty::Rights(AddressBookRight::MayRead), + b"mayWrite" => AddressBookProperty::Rights(AddressBookRight::MayWrite), + b"mayShare" => AddressBookProperty::Rights(AddressBookRight::MayShare), + b"mayDelete" => AddressBookProperty::Rights(AddressBookRight::MayDelete) + ) + .or_else(|| { + if allow_patch && value.contains('/') { + AddressBookProperty::Pointer(JsonPointer::parse(value)).into() + } else { + None + } + }) + } + + fn patch_or_prop(&self) -> &AddressBookProperty { + if let AddressBookProperty::Pointer(ptr) = self + && let Some(JsonPointerItem::Key(Key::Property(prop))) = ptr.last() + { + prop + } else { + self + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct AddressBookSetArguments { + pub on_destroy_remove_contents: Option, + pub on_success_set_is_default: Option, +} + +impl<'de> DeserializeArguments<'de> for AddressBookSetArguments { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"onDestroyRemoveContents" => { + self.on_destroy_remove_contents = map.next_value()?; + }, + b"onSuccessSetIsDefault" => { + self.on_success_set_is_default = map.next_value()?; + }, + _ => { + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + +impl FromStr for AddressBookProperty { + type Err = (); + + fn from_str(s: &str) -> Result { + AddressBookProperty::parse(s, false).ok_or(()) + } +} + +impl JmapObject for AddressBook { + type Property = AddressBookProperty; + + type Element = AddressBookValue; + + type Id = Id; + + type Filter = (); + + type Comparator = (); + + type GetArguments = (); + + type SetArguments<'de> = AddressBookSetArguments; + + type QueryArguments = (); + + type CopyArguments = (); + + type ParseArguments = (); + + const ID_PROPERTY: Self::Property = AddressBookProperty::Id; +} + +impl JmapSharedObject for AddressBook { + type Right = AddressBookRight; + + const SHARE_WITH_PROPERTY: Self::Property = AddressBookProperty::ShareWith; +} + +impl From for AddressBookProperty { + fn from(id: Id) -> Self { + AddressBookProperty::IdValue(id) + } +} + +impl TryFrom for Id { + type Error = (); + + fn try_from(value: AddressBookProperty) -> Result { + if let AddressBookProperty::IdValue(id) = value { + Ok(id) + } else { + Err(()) + } + } +} + +impl TryFrom for AddressBookRight { + type Error = (); + + fn try_from(value: AddressBookProperty) -> Result { + if let AddressBookProperty::Rights(right) = value { + Ok(right) + } else { + Err(()) + } + } +} + +impl From for AddressBookValue { + fn from(id: Id) -> Self { + AddressBookValue::Id(id) + } +} + +impl JmapObjectId for AddressBookValue { + fn as_id(&self) -> Option { + if let AddressBookValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + if let AddressBookValue::Id(id) = self { + Some(AnyId::Id(*id)) + } else { + None + } + } + + fn as_id_ref(&self) -> Option<&str> { + if let AddressBookValue::IdReference(r) = self { + Some(r) + } else { + None + } + } +} + +impl TryFrom for AddressBookValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + if let AnyId::Id(id) = value { + Ok(AddressBookValue::Id(id)) + } else { + Err(()) + } + } +} + +impl JmapRight for AddressBookRight { + fn from_acl(acl: Acl) -> &'static [Self] { + match acl { + Acl::ReadItems => &[AddressBookRight::MayRead], + Acl::RemoveItems => &[AddressBookRight::MayDelete], + Acl::ModifyItems => &[AddressBookRight::MayWrite], + Acl::Delete => &[AddressBookRight::MayDelete], + Acl::Administer => &[AddressBookRight::MayShare], + _ => &[], + } + } + + fn to_acl(&self) -> &'static [Acl] { + match self { + AddressBookRight::MayDelete => &[Acl::Delete, Acl::RemoveItems], + AddressBookRight::MayShare => &[Acl::Administer], + AddressBookRight::MayRead => &[Acl::Read, Acl::ReadItems], + AddressBookRight::MayWrite => &[Acl::Modify, Acl::AddItems, Acl::ModifyItems], + } + } + + fn all_rights() -> &'static [Self] { + &[ + AddressBookRight::MayRead, + AddressBookRight::MayWrite, + AddressBookRight::MayDelete, + AddressBookRight::MayShare, + ] + } +} + +impl From for AddressBookProperty { + fn from(right: AddressBookRight) -> Self { + AddressBookProperty::Rights(right) + } +} diff --git a/crates/jmap-proto/src/object/blob.rs b/crates/jmap-proto/src/object/blob.rs index 6336a5a9..05ed7ab0 100644 --- a/crates/jmap-proto/src/object/blob.rs +++ b/crates/jmap-proto/src/object/blob.rs @@ -183,6 +183,8 @@ impl JmapObject for Blob { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = BlobProperty::Id; } diff --git a/crates/jmap-proto/src/object/contact.rs b/crates/jmap-proto/src/object/contact.rs new file mode 100644 index 00000000..b3927f7d --- /dev/null +++ b/crates/jmap-proto/src/object/contact.rs @@ -0,0 +1,230 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + object::{AnyId, JmapObject, JmapObjectId}, + request::deserialize::DeserializeArguments, + types::date::UTCDate, +}; +use calcard::jscontact::{JSContactProperty, JSContactValue}; +use std::borrow::Cow; +use types::{blob::BlobId, id::Id}; + +#[derive(Debug, Clone, Default)] +pub struct ContactCard; + +impl JmapObject for ContactCard { + type Property = JSContactProperty; + + type Element = JSContactValue; + + type Id = Id; + + type Filter = ContactFilter; + + type Comparator = ContactComparator; + + type GetArguments = (); + + type SetArguments<'de> = (); + + type QueryArguments = (); + + type CopyArguments = (); + + type ParseArguments = (); + + const ID_PROPERTY: Self::Property = JSContactProperty::Id; +} + +impl JmapObjectId for JSContactValue { + fn as_id(&self) -> Option { + if let JSContactValue::Id(id) = self { + Some(*id) + } else { + None + } + } + + fn as_any_id(&self) -> Option { + match self { + JSContactValue::Id(id) => Some(AnyId::Id(*id)), + JSContactValue::BlobId(id) => Some(AnyId::BlobId(id.clone())), + _ => None, + } + } + + fn as_id_ref(&self) -> Option<&str> { + None + } +} + +impl TryFrom for JSContactValue { + type Error = (); + + fn try_from(value: AnyId) -> Result { + match value { + AnyId::Id(id) => Ok(JSContactValue::Id(id)), + AnyId::BlobId(id) => Ok(JSContactValue::BlobId(id)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ContactFilter { + InAddressBook(Id), + Uid(String), + HasMember(String), + Kind(String), + CreatedBefore(UTCDate), + CreatedAfter(UTCDate), + UpdatedBefore(UTCDate), + UpdatedAfter(UTCDate), + Text(String), + Name(String), + NameGiven(String), + NameSurname(String), + NameSurname2(String), + Nickname(String), + Organization(String), + Email(String), + Phone(String), + OnlineService(String), + Address(String), + Note(String), + _T(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ContactComparator { + Created, + Updated, + NameGiven, + NameSurname, + NameSurname2, + _T(String), +} + +impl<'de> DeserializeArguments<'de> for ContactFilter { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"inAddressBook" => { + *self = ContactFilter::InAddressBook(map.next_value()?); + }, + b"uid" => { + *self = ContactFilter::Uid(map.next_value()?); + }, + b"hasMember" => { + *self = ContactFilter::HasMember(map.next_value()?); + }, + b"kind" => { + *self = ContactFilter::Kind(map.next_value()?); + }, + b"createdBefore" => { + *self = ContactFilter::CreatedBefore(map.next_value()?); + }, + b"createdAfter" => { + *self = ContactFilter::CreatedAfter(map.next_value()?); + }, + b"updatedBefore" => { + *self = ContactFilter::UpdatedBefore(map.next_value()?); + }, + b"updatedAfter" => { + *self = ContactFilter::UpdatedAfter(map.next_value()?); + }, + b"text" => { + *self = ContactFilter::Text(map.next_value()?); + }, + b"name" => { + *self = ContactFilter::Name(map.next_value()?); + }, + b"name/given" => { + *self = ContactFilter::NameGiven(map.next_value()?); + }, + b"name/surname" => { + *self = ContactFilter::NameSurname(map.next_value()?); + }, + b"name/surname2" => { + *self = ContactFilter::NameSurname2(map.next_value()?); + }, + b"nickname" => { + *self = ContactFilter::Nickname(map.next_value()?); + }, + b"organization" => { + *self = ContactFilter::Organization(map.next_value()?); + }, + b"email" => { + *self = ContactFilter::Email(map.next_value()?); + }, + b"phone" => { + *self = ContactFilter::Phone(map.next_value()?); + }, + b"onlineService" => { + *self = ContactFilter::OnlineService(map.next_value()?); + }, + b"address" => { + *self = ContactFilter::Address(map.next_value()?); + }, + b"note" => { + *self = ContactFilter::Note(map.next_value()?); + }, + _ => { + *self = ContactFilter::_T(key.to_string()); + let _ = map.next_value::()?; + } + ); + Ok(()) + } +} + +impl<'de> DeserializeArguments<'de> for ContactComparator { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + if key == "property" { + let value = map.next_value::>()?; + hashify::fnc_map!(value.as_bytes(), + b"created" => { + *self = ContactComparator::Created; + }, + b"updated" => { + *self = ContactComparator::Updated; + }, + b"name/given" => { + *self = ContactComparator::NameGiven; + }, + b"name/surname" => { + *self = ContactComparator::NameSurname; + }, + b"name/surname2" => { + *self = ContactComparator::NameSurname2; + }, + _ => { + *self = ContactComparator::_T(value.to_string()); + } + ); + } else { + let _ = map.next_value::()?; + } + Ok(()) + } +} + +impl Default for ContactFilter { + fn default() -> Self { + ContactFilter::_T(String::new()) + } +} + +impl Default for ContactComparator { + fn default() -> Self { + ContactComparator::_T(String::new()) + } +} diff --git a/crates/jmap-proto/src/object/email.rs b/crates/jmap-proto/src/object/email.rs index ab284973..7ff2b680 100644 --- a/crates/jmap-proto/src/object/email.rs +++ b/crates/jmap-proto/src/object/email.rs @@ -396,6 +396,15 @@ pub struct EmailQueryArguments { pub collapse_threads: Option, } +#[derive(Debug, Clone, Default)] +pub struct EmailParseArguments { + pub body_properties: Option>>, + pub fetch_text_body_values: Option, + pub fetch_html_body_values: Option, + pub fetch_all_body_values: Option, + pub max_body_value_bytes: Option, +} + impl<'de> DeserializeArguments<'de> for EmailGetArguments { fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> where @@ -441,6 +450,36 @@ impl<'de> DeserializeArguments<'de> for EmailQueryArguments { } } +impl<'de> DeserializeArguments<'de> for EmailParseArguments { + fn deserialize_argument(&mut self, key: &str, map: &mut A) -> Result<(), A::Error> + where + A: serde::de::MapAccess<'de>, + { + hashify::fnc_map!(key.as_bytes(), + b"bodyProperties" => { + self.body_properties = map.next_value()?; + }, + b"fetchTextBodyValues" => { + self.fetch_text_body_values = map.next_value()?; + }, + b"fetchHTMLBodyValues" => { + self.fetch_html_body_values = map.next_value()?; + }, + b"fetchAllBodyValues" => { + self.fetch_all_body_values = map.next_value()?; + }, + b"maxBodyValueBytes" => { + self.max_body_value_bytes = map.next_value()?; + }, + _ => { + let _ = map.next_value::()?; + } + ); + + Ok(()) + } +} + impl serde::Serialize for EmailProperty { fn serialize(&self, serializer: S) -> Result where @@ -469,6 +508,8 @@ impl JmapObject for Email { type CopyArguments = (); + type ParseArguments = EmailParseArguments; + const ID_PROPERTY: Self::Property = EmailProperty::Id; } diff --git a/crates/jmap-proto/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs index 1677ebc6..407708f3 100644 --- a/crates/jmap-proto/src/object/email_submission.rs +++ b/crates/jmap-proto/src/object/email_submission.rs @@ -330,6 +330,8 @@ impl JmapObject for EmailSubmission { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = EmailSubmissionProperty::Id; } diff --git a/crates/jmap-proto/src/object/identity.rs b/crates/jmap-proto/src/object/identity.rs index 6a4bd4ad..72840796 100644 --- a/crates/jmap-proto/src/object/identity.rs +++ b/crates/jmap-proto/src/object/identity.rs @@ -4,12 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::object::{AnyId, JmapObject, JmapObjectId}; use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; use std::{borrow::Cow, str::FromStr}; use types::id::Id; -use crate::object::{AnyId, JmapObject, JmapObjectId}; - #[derive(Debug, Clone, Default)] pub struct Identity; @@ -143,6 +142,8 @@ impl JmapObject for Identity { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = IdentityProperty::Id; } diff --git a/crates/jmap-proto/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs index fa20594c..92084a79 100644 --- a/crates/jmap-proto/src/object/mailbox.rs +++ b/crates/jmap-proto/src/object/mailbox.rs @@ -4,17 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Cow, str::FromStr}; - -use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; -use types::{acl::Acl, id::Id, special_use::SpecialUse}; - use crate::{ object::{ AnyId, JmapObject, JmapObjectId, JmapRight, JmapSharedObject, MaybeReference, parse_ref, }, request::deserialize::DeserializeArguments, }; +use jmap_tools::{Element, JsonPointer, JsonPointerItem, Key, Property}; +use std::{borrow::Cow, str::FromStr}; +use types::{acl::Acl, id::Id, special_use::SpecialUse}; #[derive(Debug, Clone, Default)] pub struct Mailbox; @@ -272,6 +270,8 @@ impl JmapObject for Mailbox { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = MailboxProperty::Id; } diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs index b304a7b5..5d828824 100644 --- a/crates/jmap-proto/src/object/mod.rs +++ b/crates/jmap-proto/src/object/mod.rs @@ -10,7 +10,9 @@ use serde::Serialize; use std::{fmt::Debug, str::FromStr}; use types::{acl::Acl, blob::BlobId, id::Id}; +pub mod addressbook; pub mod blob; +pub mod contact; pub mod email; pub mod email_submission; pub mod identity; @@ -24,7 +26,7 @@ pub mod thread; pub mod vacation_response; pub trait JmapObject: std::fmt::Debug { - type Property: Property + FromStr + Serialize + Debug + Sync + Send; + type Property: Property + FromStr + Debug + Sync + Send; type Element: Element + From + JmapObjectId @@ -40,6 +42,7 @@ pub trait JmapObject: std::fmt::Debug { type SetArguments<'de>: Default + DeserializeArguments<'de> + Debug + Sync + Send; type QueryArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; type CopyArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; + type ParseArguments: Default + for<'de> DeserializeArguments<'de> + Debug + Sync + Send; const ID_PROPERTY: Self::Property; } @@ -156,6 +159,7 @@ impl JmapObject for NullObject { type SetArguments<'de> = (); type QueryArguments = (); type CopyArguments = (); + type ParseArguments = (); const ID_PROPERTY: Self::Property = Null; } diff --git a/crates/jmap-proto/src/object/principal.rs b/crates/jmap-proto/src/object/principal.rs index 49566553..4bdc0297 100644 --- a/crates/jmap-proto/src/object/principal.rs +++ b/crates/jmap-proto/src/object/principal.rs @@ -156,6 +156,8 @@ impl JmapObject for Principal { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = PrincipalProperty::Id; } diff --git a/crates/jmap-proto/src/object/push_subscription.rs b/crates/jmap-proto/src/object/push_subscription.rs index a52a2f18..542ab2a1 100644 --- a/crates/jmap-proto/src/object/push_subscription.rs +++ b/crates/jmap-proto/src/object/push_subscription.rs @@ -162,6 +162,8 @@ impl JmapObject for PushSubscription { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = PushSubscriptionProperty::Id; } diff --git a/crates/jmap-proto/src/object/quota.rs b/crates/jmap-proto/src/object/quota.rs index 3cbfd8f7..8b09d038 100644 --- a/crates/jmap-proto/src/object/quota.rs +++ b/crates/jmap-proto/src/object/quota.rs @@ -133,6 +133,8 @@ impl JmapObject for Quota { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = QuotaProperty::Id; } diff --git a/crates/jmap-proto/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs index d267ec99..89271bdf 100644 --- a/crates/jmap-proto/src/object/sieve.rs +++ b/crates/jmap-proto/src/object/sieve.rs @@ -152,6 +152,8 @@ impl JmapObject for Sieve { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = SieveProperty::Id; } diff --git a/crates/jmap-proto/src/object/thread.rs b/crates/jmap-proto/src/object/thread.rs index c1a08d1b..bef0783c 100644 --- a/crates/jmap-proto/src/object/thread.rs +++ b/crates/jmap-proto/src/object/thread.rs @@ -101,6 +101,8 @@ impl JmapObject for Thread { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = ThreadProperty::Id; } diff --git a/crates/jmap-proto/src/object/vacation_response.rs b/crates/jmap-proto/src/object/vacation_response.rs index 628ddf8c..a451f5d5 100644 --- a/crates/jmap-proto/src/object/vacation_response.rs +++ b/crates/jmap-proto/src/object/vacation_response.rs @@ -130,6 +130,8 @@ impl JmapObject for VacationResponse { type CopyArguments = (); + type ParseArguments = (); + const ID_PROPERTY: Self::Property = VacationResponseProperty::Id; } diff --git a/crates/jmap-proto/src/references/eval.rs b/crates/jmap-proto/src/references/eval.rs index f5c41d9a..85f2dbf4 100644 --- a/crates/jmap-proto/src/references/eval.rs +++ b/crates/jmap-proto/src/references/eval.rs @@ -57,6 +57,12 @@ impl Response<'_> { response.eval_jptr(path, &mut results) } GetResponseMethod::Blob(response) => response.eval_jptr(path, &mut results), + GetResponseMethod::AddressBook(response) => { + response.eval_jptr(path, &mut results) + } + GetResponseMethod::ContactCard(response) => { + response.eval_jptr(path, &mut results) + } }, ResponseMethod::Changes(response) => match response { ChangesResponseMethod::Email(response) => { @@ -77,6 +83,12 @@ impl Response<'_> { ChangesResponseMethod::Quota(response) => { response.eval_jptr(path, &mut results) } + ChangesResponseMethod::AddressBook(response) => { + response.eval_jptr(path, &mut results) + } + ChangesResponseMethod::ContactCard(response) => { + response.eval_jptr(path, &mut results) + } }, ResponseMethod::Query(response) => response.eval_jptr(path, &mut results), ResponseMethod::QueryChanges(response) => { diff --git a/crates/jmap-proto/src/references/jsptr.rs b/crates/jmap-proto/src/references/jsptr.rs index b376e24b..b66b7c78 100644 --- a/crates/jmap-proto/src/references/jsptr.rs +++ b/crates/jmap-proto/src/references/jsptr.rs @@ -136,7 +136,7 @@ impl ResponsePtr for BlobId { } } -impl ResponsePtr for PropertyWrapper { +impl ResponsePtr for PropertyWrapper { fn eval_jptr(&self, _: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool { results.0.push(EvalResult::Property(self.0.to_cow())); true diff --git a/crates/jmap-proto/src/references/resolve.rs b/crates/jmap-proto/src/references/resolve.rs index e8d48ff6..f20abbcf 100644 --- a/crates/jmap-proto/src/references/resolve.rs +++ b/crates/jmap-proto/src/references/resolve.rs @@ -42,6 +42,8 @@ impl Response<'_> { GetRequestMethod::Principal(request) => request.resolve_references(self)?, GetRequestMethod::Quota(request) => request.resolve_references(self)?, GetRequestMethod::Blob(request) => request.resolve_references(self)?, + GetRequestMethod::AddressBook(request) => request.resolve_references(self)?, + GetRequestMethod::ContactCard(request) => request.resolve_references(self)?, }, RequestMethod::Set(request) => match request { SetRequestMethod::Email(request) => request.resolve_references(self)?, @@ -51,9 +53,12 @@ impl Response<'_> { SetRequestMethod::PushSubscription(request) => request.resolve_references(self)?, SetRequestMethod::Sieve(request) => request.resolve_references(self)?, SetRequestMethod::VacationResponse(request) => request.resolve_references(self)?, + SetRequestMethod::AddressBook(request) => request.resolve_references(self)?, + SetRequestMethod::ContactCard(request) => request.resolve_references(self)?, }, RequestMethod::Copy(request) => match request { CopyRequestMethod::Email(request) => request.resolve_references(self)?, + CopyRequestMethod::ContactCard(request) => request.resolve_references(self)?, CopyRequestMethod::Blob(_) => (), }, RequestMethod::ImportEmail(request) => request.resolve_references(self)?, diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index 3f0404ab..8a79b430 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -70,6 +70,10 @@ pub enum Capability { Blob = 1 << 8, #[serde(rename(serialize = "urn:ietf:params:jmap:quota"))] Quota = 1 << 9, + #[serde(rename(serialize = "urn:ietf:params:jmap:principals"))] + Principals = 1 << 10, + #[serde(rename(serialize = "urn:ietf:params:jmap:principals:owner"))] + PrincipalsOwner = 1 << 11, } #[derive(Debug, Clone, Copy, Default)] @@ -87,6 +91,9 @@ pub enum Capabilities { SieveAccount(SieveAccountCapabilities), SieveSession(SieveSessionCapabilities), Blob(BlobCapabilities), + Contacts(ContactsCapabilities), + Principals(PrincipalsCapabilities), + PrincipalsOwner(PrincipalsOwnerCapabilities), Empty(EmptyCapabilities), } @@ -178,6 +185,29 @@ pub struct BlobCapabilities { pub supported_digest_algorithms: Vec<&'static str>, } +#[derive(Debug, Clone, serde::Serialize)] +pub struct ContactsCapabilities { + #[serde(rename(serialize = "maxAddressBooksPerCard"))] + pub max_address_books_per_card: Option, + #[serde(rename(serialize = "mayCreateAddressBook"))] + pub may_create_address_book: bool, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct PrincipalsCapabilities { + #[serde(rename(serialize = "currentUserPrincipalId"))] + pub current_user_principal_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct PrincipalsOwnerCapabilities { + #[serde(rename(serialize = "accountIdForPrincipal"))] + pub account_id_for_principal: Id, + + #[serde(rename(serialize = "principalId"))] + pub principal_id: Id, +} + #[derive(Debug, Clone, Default, serde::Serialize)] pub struct EmptyCapabilities {} @@ -332,6 +362,8 @@ impl Capability { "urn:ietf:params:jmap:sieve" => Capability::Sieve, "urn:ietf:params:jmap:blob" => Capability::Blob, "urn:ietf:params:jmap:quota" => Capability::Quota, + "urn:ietf:params:jmap:principals" => Capability::Principals, + "urn:ietf:params:jmap:principals:owner" => Capability::PrincipalsOwner ) } } diff --git a/crates/jmap-proto/src/request/method.rs b/crates/jmap-proto/src/request/method.rs index b1c32866..284bbf0c 100644 --- a/crates/jmap-proto/src/request/method.rs +++ b/crates/jmap-proto/src/request/method.rs @@ -27,6 +27,8 @@ pub enum MethodObject { SieveScript, Principal, Quota, + AddressBook, + ContactCard, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -122,6 +124,18 @@ impl MethodName { (MethodFunction::Lookup, MethodObject::Blob) => "Blob/lookup", (MethodFunction::Upload, MethodObject::Blob) => "Blob/upload", + (MethodFunction::Get, MethodObject::AddressBook) => "AddressBook/get", + (MethodFunction::Changes, MethodObject::AddressBook) => "AddressBook/changes", + (MethodFunction::Set, MethodObject::AddressBook) => "AddressBook/set", + + (MethodFunction::Get, MethodObject::ContactCard) => "ContactCard/get", + (MethodFunction::Changes, MethodObject::ContactCard) => "ContactCard/changes", + (MethodFunction::Query, MethodObject::ContactCard) => "ContactCard/query", + (MethodFunction::QueryChanges, MethodObject::ContactCard) => "ContactCard/queryChanges", + (MethodFunction::Set, MethodObject::ContactCard) => "ContactCard/set", + (MethodFunction::Copy, MethodObject::ContactCard) => "ContactCard/copy", + (MethodFunction::Parse, MethodObject::ContactCard) => "ContactCard/parse", + (MethodFunction::Echo, MethodObject::Core) => "Core/echo", _ => "error", } @@ -184,6 +198,18 @@ impl MethodName { "Blob/lookup" => (MethodObject::Blob, MethodFunction::Lookup), "Blob/upload" => (MethodObject::Blob, MethodFunction::Upload), + "AddressBook/get" => (MethodObject::AddressBook, MethodFunction::Get), + "AddressBook/changes" => (MethodObject::AddressBook, MethodFunction::Changes), + "AddressBook/set" => (MethodObject::AddressBook, MethodFunction::Set), + + "ContactCard/get" => (MethodObject::ContactCard, MethodFunction::Get), + "ContactCard/changes" => (MethodObject::ContactCard, MethodFunction::Changes), + "ContactCard/query" => (MethodObject::ContactCard, MethodFunction::Query), + "ContactCard/queryChanges" => (MethodObject::ContactCard, MethodFunction::QueryChanges), + "ContactCard/set" => (MethodObject::ContactCard, MethodFunction::Set), + "ContactCard/copy" => (MethodObject::ContactCard, MethodFunction::Copy), + "ContactCard/parse" => (MethodObject::ContactCard, MethodFunction::Parse), + "Core/echo" => (MethodObject::Core, MethodFunction::Echo), ).map(|(obj, fnc)| MethodName { obj, fnc }) @@ -207,6 +233,8 @@ impl Display for MethodObject { MethodObject::Thread => "Thread", MethodObject::Email => "Email", MethodObject::Quota => "Quota", + MethodObject::AddressBook => "AddressBook", + MethodObject::ContactCard => "ContactCard", }) } } diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index fe2d791a..23344fac 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -19,7 +19,7 @@ use crate::{ get::GetRequest, import::ImportEmailRequest, lookup::BlobLookupRequest, - parse::ParseEmailRequest, + parse::ParseRequest, query::QueryRequest, query_changes::QueryChangesRequest, search_snippet::GetSearchSnippetRequest, @@ -28,9 +28,10 @@ use crate::{ validate::ValidateSieveScriptRequest, }, object::{ - AnyId, blob::Blob, email::Email, email_submission::EmailSubmission, identity::Identity, - mailbox::Mailbox, principal::Principal, push_subscription::PushSubscription, quota::Quota, - sieve::Sieve, thread::Thread, vacation_response::VacationResponse, + AnyId, addressbook::AddressBook, blob::Blob, contact::ContactCard, email::Email, + email_submission::EmailSubmission, identity::Identity, mailbox::Mailbox, + principal::Principal, push_subscription::PushSubscription, quota::Quota, sieve::Sieve, + thread::Thread, vacation_response::VacationResponse, }, request::{capability::CapabilityIds, reference::MaybeIdReference}, }; @@ -59,7 +60,7 @@ pub enum RequestMethod<'x> { Changes(ChangesRequest), Copy(CopyRequestMethod<'x>), ImportEmail(ImportEmailRequest), - ParseEmail(ParseEmailRequest), + Parse(ParseRequestMethod), Query(QueryRequestMethod), QueryChanges(QueryChangesRequestMethod), SearchSnippet(GetSearchSnippetRequest), @@ -83,6 +84,8 @@ pub enum GetRequestMethod { Principal(GetRequest), Quota(GetRequest), Blob(GetRequest), + AddressBook(GetRequest), + ContactCard(GetRequest), } #[derive(Debug)] @@ -94,11 +97,14 @@ pub enum SetRequestMethod<'x> { PushSubscription(SetRequest<'x, PushSubscription>), Sieve(SetRequest<'x, Sieve>), VacationResponse(SetRequest<'x, VacationResponse>), + AddressBook(SetRequest<'x, AddressBook>), + ContactCard(SetRequest<'x, ContactCard>), } #[derive(Debug)] pub enum CopyRequestMethod<'x> { Email(CopyRequest<'x, Email>), + ContactCard(CopyRequest<'x, ContactCard>), Blob(CopyBlobRequest), } @@ -110,6 +116,7 @@ pub enum QueryRequestMethod { Sieve(QueryRequest), Principal(QueryRequest), Quota(QueryRequest), + ContactCard(QueryRequest), } #[derive(Debug)] @@ -120,6 +127,13 @@ pub enum QueryChangesRequestMethod { Sieve(QueryChangesRequest), Principal(QueryChangesRequest), Quota(QueryChangesRequest), + ContactCard(QueryChangesRequest), +} + +#[derive(Debug)] +pub enum ParseRequestMethod { + Email(ParseRequest), + ContactCard(ParseRequest), } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index f96a6933..8502afa4 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -9,8 +9,8 @@ use super::{ method::{MethodFunction, MethodName, MethodObject}, }; use crate::request::{ - CopyRequestMethod, GetRequestMethod, QueryChangesRequestMethod, QueryRequestMethod, - SetRequestMethod, + CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod, + QueryRequestMethod, SetRequestMethod, deserialize::{DeserializeArguments, deserialize_request}, }; use serde::{ @@ -181,6 +181,20 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Get, MethodObject::AddressBook) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::AddressBook(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::Get, MethodObject::ContactCard) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::ContactCard(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::Get, MethodObject::SearchSnippet) => match seq.next_element() { Ok(Some(value)) => RequestMethod::SearchSnippet(value), Err(err) => RequestMethod::invalid(err), @@ -237,6 +251,20 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Set, MethodObject::AddressBook) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::AddressBook(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::Set, MethodObject::ContactCard) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::ContactCard(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::Query, MethodObject::Email) => match seq.next_element() { Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::Email(value)), Err(err) => RequestMethod::invalid(err), @@ -279,6 +307,13 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Query, MethodObject::ContactCard) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Query(QueryRequestMethod::ContactCard(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::QueryChanges, MethodObject::Email) => match seq.next_element() { Ok(Some(value)) => { RequestMethod::QueryChanges(QueryChangesRequestMethod::Email(value)) @@ -335,6 +370,15 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::QueryChanges, MethodObject::ContactCard) => match seq.next_element() { + Ok(Some(value)) => { + RequestMethod::QueryChanges(QueryChangesRequestMethod::ContactCard(value)) + } + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::Changes, _) => match seq.next_element() { Ok(Some(value)) => RequestMethod::Changes(value), Err(err) => RequestMethod::invalid(err), @@ -356,6 +400,13 @@ impl<'de> Visitor<'de> for CallVisitor { return Err(de::Error::invalid_length(1, &self)); } }, + (MethodFunction::Copy, MethodObject::ContactCard) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Copy(CopyRequestMethod::ContactCard(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, (MethodFunction::Lookup, MethodObject::Blob) => match seq.next_element() { Ok(Some(value)) => RequestMethod::LookupBlob(value), Err(err) => RequestMethod::invalid(err), @@ -378,7 +429,14 @@ impl<'de> Visitor<'de> for CallVisitor { } }, (MethodFunction::Parse, MethodObject::Email) => match seq.next_element() { - Ok(Some(value)) => RequestMethod::ParseEmail(value), + Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::Email(value)), + Err(err) => RequestMethod::invalid(err), + Ok(None) => { + return Err(de::Error::invalid_length(1, &self)); + } + }, + (MethodFunction::Parse, MethodObject::ContactCard) => match seq.next_element() { + Ok(Some(value)) => RequestMethod::Parse(ParseRequestMethod::ContactCard(value)), Err(err) => RequestMethod::invalid(err), Ok(None) => { return Err(de::Error::invalid_length(1, &self)); diff --git a/crates/jmap-proto/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs index a52c9bff..b73dfd71 100644 --- a/crates/jmap-proto/src/response/mod.rs +++ b/crates/jmap-proto/src/response/mod.rs @@ -16,7 +16,7 @@ use crate::{ get::GetResponse, import::ImportEmailResponse, lookup::BlobLookupResponse, - parse::ParseEmailResponse, + parse::ParseResponse, query::QueryResponse, query_changes::QueryChangesResponse, search_snippet::GetSearchSnippetResponse, @@ -25,9 +25,10 @@ use crate::{ validate::ValidateSieveScriptResponse, }, object::{ - AnyId, blob::Blob, email::Email, email_submission::EmailSubmission, identity::Identity, - mailbox::Mailbox, principal::Principal, push_subscription::PushSubscription, quota::Quota, - sieve::Sieve, thread::Thread, vacation_response::VacationResponse, + AnyId, addressbook::AddressBook, blob::Blob, contact::ContactCard, email::Email, + email_submission::EmailSubmission, identity::Identity, mailbox::Mailbox, + principal::Principal, push_subscription::PushSubscription, quota::Quota, sieve::Sieve, + thread::Thread, vacation_response::VacationResponse, }, request::{Call, method::MethodName}, }; @@ -42,7 +43,7 @@ pub enum ResponseMethod<'x> { Changes(ChangesResponseMethod), Copy(CopyResponseMethod), ImportEmail(ImportEmailResponse), - ParseEmail(ParseEmailResponse), + Parse(ParseResponseMethod), QueryChanges(QueryChangesResponse), Query(QueryResponse), SearchSnippet(GetSearchSnippetResponse), @@ -67,6 +68,8 @@ pub enum GetResponseMethod { Principal(GetResponse), Quota(GetResponse), Blob(GetResponse), + AddressBook(GetResponse), + ContactCard(GetResponse), } #[derive(Debug, serde::Serialize)] @@ -79,6 +82,8 @@ pub enum SetResponseMethod { PushSubscription(SetResponse), Sieve(SetResponse), VacationResponse(SetResponse), + AddressBook(SetResponse), + ContactCard(SetResponse), } #[derive(Debug, serde::Serialize)] @@ -90,15 +95,25 @@ pub enum ChangesResponseMethod { Identity(ChangesResponse), EmailSubmission(ChangesResponse), Quota(ChangesResponse), + AddressBook(ChangesResponse), + ContactCard(ChangesResponse), } #[derive(Debug, serde::Serialize)] #[serde(untagged)] pub enum CopyResponseMethod { Email(CopyResponse), + ContactCard(CopyResponse), Blob(CopyBlobResponse), } +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum ParseResponseMethod { + Email(ParseResponse), + ContactCard(ParseResponse), +} + #[derive(Debug, serde::Serialize)] pub struct Response<'x> { #[serde(rename = "methodResponses")] @@ -229,7 +244,18 @@ impl<'x> From> for ResponseMethod<'x> { } } -// Direct SetResponse conversions to ResponseMethod +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::ContactCard(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: GetResponse) -> Self { + ResponseMethod::Get(GetResponseMethod::AddressBook(value)) + } +} + impl<'x> From> for ResponseMethod<'x> { fn from(value: SetResponse) -> Self { ResponseMethod::Set(SetResponseMethod::Email(value)) @@ -272,7 +298,18 @@ impl<'x> From> for ResponseMethod<'x> { } } -// Direct ChangesResponse conversions to ResponseMethod +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::AddressBook(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: SetResponse) -> Self { + ResponseMethod::Set(SetResponseMethod::ContactCard(value)) + } +} + impl<'x> From> for ResponseMethod<'x> { fn from(value: ChangesResponse) -> Self { ResponseMethod::Changes(ChangesResponseMethod::Email(value)) @@ -309,7 +346,12 @@ impl<'x> From> for ResponseMethod<'x> { } } -// Direct CopyResponse conversions to ResponseMethod +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ChangesResponse) -> Self { + ResponseMethod::Changes(ChangesResponseMethod::AddressBook(value)) + } +} + impl<'x> From> for ResponseMethod<'x> { fn from(value: CopyResponse) -> Self { ResponseMethod::Copy(CopyResponseMethod::Email(value)) @@ -322,16 +364,27 @@ impl<'x> From for ResponseMethod<'x> { } } -// Other direct conversions +impl<'x> From> for ResponseMethod<'x> { + fn from(value: CopyResponse) -> Self { + ResponseMethod::Copy(CopyResponseMethod::ContactCard(value)) + } +} + impl<'x> From for ResponseMethod<'x> { fn from(value: ImportEmailResponse) -> Self { ResponseMethod::ImportEmail(value) } } -impl<'x> From for ResponseMethod<'x> { - fn from(value: ParseEmailResponse) -> Self { - ResponseMethod::ParseEmail(value) +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ParseResponse) -> Self { + ResponseMethod::Parse(ParseResponseMethod::Email(value)) + } +} + +impl<'x> From> for ResponseMethod<'x> { + fn from(value: ParseResponse) -> Self { + ResponseMethod::Parse(ParseResponseMethod::ContactCard(value)) } } diff --git a/crates/jmap/src/addressbook/get.rs b/crates/jmap/src/addressbook/get.rs new file mode 100644 index 00000000..42f25fec --- /dev/null +++ b/crates/jmap/src/addressbook/get.rs @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::get::{GetRequest, GetResponse}, + object::addressbook::AddressBook, +}; + +pub trait AddressBookGet: Sync + Send { + fn address_book_get( + &self, + request: GetRequest, + access_token: &AccessToken, + ) -> impl Future>> + Send; +} + +impl AddressBookGet for Server { + async fn address_book_get( + &self, + mut request: GetRequest, + access_token: &AccessToken, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/addressbook/mod.rs b/crates/jmap/src/addressbook/mod.rs new file mode 100644 index 00000000..f460abaa --- /dev/null +++ b/crates/jmap/src/addressbook/mod.rs @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod get; +pub mod set; diff --git a/crates/jmap/src/addressbook/set.rs b/crates/jmap/src/addressbook/set.rs new file mode 100644 index 00000000..543f0926 --- /dev/null +++ b/crates/jmap/src/addressbook/set.rs @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use http_proto::HttpSessionData; +use jmap_proto::{ + method::set::{SetRequest, SetResponse}, + object::addressbook::AddressBook, +}; + +pub trait AddressBookSet: Sync + Send { + fn address_book_set( + &self, + request: SetRequest<'_, AddressBook>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future>> + Send; +} + +impl AddressBookSet for Server { + async fn address_book_set( + &self, + mut request: SetRequest<'_, AddressBook>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/api/auth.rs b/crates/jmap/src/api/auth.rs index 722dfc27..77b751c8 100644 --- a/crates/jmap/src/api/auth.rs +++ b/crates/jmap/src/api/auth.rs @@ -7,8 +7,8 @@ use common::auth::AccessToken; use directory::Permission; use jmap_proto::request::{ - CopyRequestMethod, GetRequestMethod, QueryChangesRequestMethod, QueryRequestMethod, - RequestMethod, SetRequestMethod, method::MethodObject, + CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod, + QueryRequestMethod, RequestMethod, SetRequestMethod, method::MethodObject, }; use types::{collection::Collection, id::Id}; @@ -67,6 +67,8 @@ impl JmapAuthorization for AccessToken { GetRequestMethod::Principal(_) => Permission::JmapPrincipalGet, GetRequestMethod::Quota(_) => Permission::JmapQuotaGet, GetRequestMethod::Blob(_) => Permission::JmapBlobGet, + GetRequestMethod::AddressBook(_) => Permission::JmapAddressBookGet, + GetRequestMethod::ContactCard(_) => Permission::JmapContactCardGet, }, RequestMethod::Set(m) => match &m { SetRequestMethod::Email(_) => Permission::JmapEmailSet, @@ -76,6 +78,8 @@ impl JmapAuthorization for AccessToken { SetRequestMethod::PushSubscription(_) => Permission::JmapPushSubscriptionSet, SetRequestMethod::Sieve(_) => Permission::JmapSieveScriptSet, SetRequestMethod::VacationResponse(_) => Permission::JmapVacationResponseSet, + SetRequestMethod::AddressBook(_) => Permission::JmapAddressBookSet, + SetRequestMethod::ContactCard(_) => Permission::JmapContactCardSet, }, RequestMethod::Changes(_) => match object { MethodObject::Email => Permission::JmapEmailChanges, @@ -84,20 +88,26 @@ impl JmapAuthorization for AccessToken { MethodObject::Identity => Permission::JmapIdentityChanges, MethodObject::EmailSubmission => Permission::JmapEmailSubmissionChanges, MethodObject::Quota => Permission::JmapQuotaChanges, + MethodObject::ContactCard => Permission::JmapContactCardChanges, MethodObject::Core | MethodObject::Blob | MethodObject::PushSubscription | MethodObject::SearchSnippet | MethodObject::VacationResponse | MethodObject::SieveScript - | MethodObject::Principal => Permission::JmapEmailChanges, // Unimplemented + | MethodObject::Principal + | MethodObject::AddressBook => Permission::JmapEmailChanges, }, RequestMethod::Copy(m) => match &m { CopyRequestMethod::Email(_) => Permission::JmapEmailCopy, CopyRequestMethod::Blob(_) => Permission::JmapBlobCopy, + CopyRequestMethod::ContactCard(_) => Permission::JmapContactCardCopy, }, RequestMethod::ImportEmail(_) => Permission::JmapEmailImport, - RequestMethod::ParseEmail(_) => Permission::JmapEmailParse, + RequestMethod::Parse(m) => match &m { + ParseRequestMethod::Email(_) => Permission::JmapEmailParse, + ParseRequestMethod::ContactCard(_) => Permission::JmapContactCardParse, + }, RequestMethod::QueryChanges(m) => match m { QueryChangesRequestMethod::Email(_) => Permission::JmapEmailQueryChanges, QueryChangesRequestMethod::Mailbox(_) => Permission::JmapMailboxQueryChanges, @@ -107,6 +117,9 @@ impl JmapAuthorization for AccessToken { QueryChangesRequestMethod::Sieve(_) => Permission::JmapSieveScriptQueryChanges, QueryChangesRequestMethod::Principal(_) => Permission::JmapPrincipalQueryChanges, QueryChangesRequestMethod::Quota(_) => Permission::JmapQuotaQueryChanges, + QueryChangesRequestMethod::ContactCard(_) => { + Permission::JmapContactCardQueryChanges + } }, RequestMethod::Query(m) => match m { QueryRequestMethod::Email(_) => Permission::JmapEmailQuery, @@ -115,6 +128,7 @@ impl JmapAuthorization for AccessToken { QueryRequestMethod::Sieve(_) => Permission::JmapSieveScriptQuery, QueryRequestMethod::Principal(_) => Permission::JmapPrincipalQuery, QueryRequestMethod::Quota(_) => Permission::JmapQuotaQuery, + QueryRequestMethod::ContactCard(_) => Permission::JmapContactCardQuery, }, RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet, RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate, diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 3acf54fa..3a3280ff 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -4,12 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{sync::Arc, time::Instant}; - use crate::{ + addressbook::{get::AddressBookGet, set::AddressBookSet}, api::auth::JmapAuthorization, blob::{copy::BlobCopy, get::BlobOperations, upload::BlobUpload}, changes::{get::ChangesLookup, query::QueryChanges}, + contact::{ + copy::JmapContactCardCopy, get::ContactCardGet, parse::ContactCardParse, + query::ContactCardQuery, set::ContactCardSet, + }, email::{ copy::JmapEmailCopy, get::EmailGet, import::EmailImport, parse::EmailParse, query::EmailQuery, set::EmailSet, snippet::EmailSearchSnippet, @@ -31,16 +34,16 @@ use common::{Server, auth::AccessToken}; use http_proto::HttpSessionData; use jmap_proto::{ request::{ - Call, CopyRequestMethod, GetRequestMethod, QueryRequestMethod, Request, RequestMethod, - SetRequestMethod, method::MethodName, + Call, CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryRequestMethod, Request, + RequestMethod, SetRequestMethod, method::MethodName, }, response::{Response, ResponseMethod, SetResponseMethod}, }; +use std::future::Future; +use std::{sync::Arc, time::Instant}; use trc::JmapEvent; use types::{collection::Collection, id::Id}; -use std::future::Future; - pub trait RequestHandler: Sync + Send { fn handle_jmap_request<'x>( &self, @@ -126,6 +129,12 @@ impl RequestHandler for Server { SetResponseMethod::VacationResponse(set_response) => { set_response.update_created_ids(&mut response); } + SetResponseMethod::AddressBook(set_response) => { + set_response.update_created_ids(&mut response); + } + SetResponseMethod::ContactCard(set_response) => { + set_response.update_created_ids(&mut response); + } } } ResponseMethod::ImportEmail(import_response) => { @@ -248,6 +257,18 @@ impl RequestHandler for Server { self.blob_get(req, access_token).await?.into() } + GetRequestMethod::AddressBook(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::AddressBook)?; + + self.address_book_get(req, access_token).await?.into() + } + GetRequestMethod::ContactCard(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::ContactCard)?; + + self.contact_card_get(req, access_token).await?.into() + } }, RequestMethod::Query(req) => match req { QueryRequestMethod::Email(mut req) => { @@ -284,6 +305,12 @@ impl RequestHandler for Server { self.quota_query(req, access_token).await?.into() } + QueryRequestMethod::ContactCard(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::ContactCard)?; + + self.contact_card_query(req, access_token).await?.into() + } }, RequestMethod::Set(req) => match req { SetRequestMethod::Email(mut req) => { @@ -330,6 +357,22 @@ impl RequestHandler for Server { self.vacation_response_set(req, access_token).await?.into() } + SetRequestMethod::AddressBook(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::AddressBook)?; + + self.address_book_set(req, access_token, session) + .await? + .into() + } + SetRequestMethod::ContactCard(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::ContactCard)?; + + self.contact_card_set(req, access_token, session) + .await? + .into() + } }, RequestMethod::Changes(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); @@ -356,6 +399,16 @@ impl RequestHandler for Server { self.blob_copy(req, access_token).await?.into() } + CopyRequestMethod::ContactCard(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token + .assert_has_access(req.account_id, Collection::ContactCard)? + .assert_has_access(req.from_account_id, Collection::ContactCard)?; + + self.contact_card_copy(req, access_token, next_call, session) + .await? + .into() + } }, RequestMethod::ImportEmail(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); @@ -363,12 +416,20 @@ impl RequestHandler for Server { self.email_import(req, access_token, session).await?.into() } - RequestMethod::ParseEmail(mut req) => { - set_account_id_if_missing(&mut req.account_id, access_token); - access_token.assert_has_access(req.account_id, Collection::Email)?; + RequestMethod::Parse(req) => match req { + ParseRequestMethod::Email(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::Email)?; - self.email_parse(req, access_token).await?.into() - } + self.email_parse(req, access_token).await?.into() + } + ParseRequestMethod::ContactCard(mut req) => { + set_account_id_if_missing(&mut req.account_id, access_token); + access_token.assert_has_access(req.account_id, Collection::ContactCard)?; + + self.contact_card_parse(req, access_token).await?.into() + } + }, RequestMethod::QueryChanges(req) => self.query_changes(req, access_token).await?.into(), RequestMethod::SearchSnippet(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs index 39276c26..13988619 100644 --- a/crates/jmap/src/changes/get.rs +++ b/crates/jmap/src/changes/get.rs @@ -230,6 +230,9 @@ impl IntermediateChangesResponse { MethodObject::EmailSubmission => { ChangesResponseMethod::EmailSubmission(transmute_response(self.response)) } + MethodObject::ContactCard => { + ChangesResponseMethod::ContactCard(transmute_response(self.response)) + } MethodObject::Core | MethodObject::Blob | MethodObject::PushSubscription @@ -237,7 +240,8 @@ impl IntermediateChangesResponse { | MethodObject::VacationResponse | MethodObject::SieveScript | MethodObject::Principal - | MethodObject::Quota => unreachable!(), + | MethodObject::Quota + | MethodObject::AddressBook => unreachable!(), }) } } diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index f14bd2f9..c775bf34 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -6,8 +6,8 @@ use super::get::ChangesLookup; use crate::{ - api::request::set_account_id_if_missing, email::query::EmailQuery, - mailbox::query::MailboxQuery, sieve::query::SieveScriptQuery, + api::request::set_account_id_if_missing, contact::query::ContactCardQuery, + email::query::EmailQuery, mailbox::query::MailboxQuery, sieve::query::SieveScriptQuery, submission::query::EmailSubmissionQuery, }; use common::{Server, auth::AccessToken}; @@ -137,6 +137,30 @@ impl QueryChanges for Server { up_to_id = request.up_to_id; results = self.sieve_script_query(request.into()).await?; } + QueryChangesRequestMethod::ContactCard(mut request) => { + // Query changes + set_account_id_if_missing(&mut request.account_id, access_token); + changes = self + .changes( + build_changes_request(&request), + MethodObject::ContactCard, + access_token, + ) + .await? + .response; + let calculate_total = request.calculate_total.unwrap_or(false); + has_changes = changes.has_changes(); + response = build_query_changes_response(&request, &changes); + + if !has_changes && !calculate_total { + return Ok(response); + } + + up_to_id = request.up_to_id; + results = self + .contact_card_query(request.into(), access_token) + .await?; + } QueryChangesRequestMethod::Principal(_) => { return Err(trc::JmapEvent::CannotCalculateChanges.into_err()); } diff --git a/crates/jmap/src/contact/copy.rs b/crates/jmap/src/contact/copy.rs new file mode 100644 index 00000000..5c717c61 --- /dev/null +++ b/crates/jmap/src/contact/copy.rs @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use http_proto::HttpSessionData; +use jmap_proto::{ + method::copy::{CopyRequest, CopyResponse}, + object::contact::ContactCard, + request::{Call, RequestMethod}, +}; + +pub trait JmapContactCardCopy: Sync + Send { + fn contact_card_copy<'x>( + &self, + request: CopyRequest<'x, ContactCard>, + access_token: &AccessToken, + next_call: &mut Option>>, + session: &HttpSessionData, + ) -> impl Future>> + Send; +} + +impl JmapContactCardCopy for Server { + async fn contact_card_copy<'x>( + &self, + request: CopyRequest<'x, ContactCard>, + access_token: &AccessToken, + next_call: &mut Option>>, + session: &HttpSessionData, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/contact/get.rs b/crates/jmap/src/contact/get.rs new file mode 100644 index 00000000..6a5d5c6f --- /dev/null +++ b/crates/jmap/src/contact/get.rs @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::get::{GetRequest, GetResponse}, + object::contact::ContactCard, +}; + +pub trait ContactCardGet: Sync + Send { + fn contact_card_get( + &self, + request: GetRequest, + access_token: &AccessToken, + ) -> impl Future>> + Send; +} + +impl ContactCardGet for Server { + async fn contact_card_get( + &self, + mut request: GetRequest, + access_token: &AccessToken, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/contact/mod.rs b/crates/jmap/src/contact/mod.rs new file mode 100644 index 00000000..58b319ed --- /dev/null +++ b/crates/jmap/src/contact/mod.rs @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod copy; +pub mod get; +pub mod parse; +pub mod query; +pub mod set; diff --git a/crates/jmap/src/contact/parse.rs b/crates/jmap/src/contact/parse.rs new file mode 100644 index 00000000..640f8bab --- /dev/null +++ b/crates/jmap/src/contact/parse.rs @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::parse::{ParseRequest, ParseResponse}, + object::contact::ContactCard, +}; + +pub trait ContactCardParse: Sync + Send { + fn contact_card_parse( + &self, + request: ParseRequest, + access_token: &AccessToken, + ) -> impl Future>> + Send; +} + +impl ContactCardParse for Server { + async fn contact_card_parse( + &self, + request: ParseRequest, + access_token: &AccessToken, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/contact/query.rs b/crates/jmap/src/contact/query.rs new file mode 100644 index 00000000..8aba5dc1 --- /dev/null +++ b/crates/jmap/src/contact/query.rs @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use jmap_proto::{ + method::query::{QueryRequest, QueryResponse}, + object::contact::ContactCard, +}; + +pub trait ContactCardQuery: Sync + Send { + fn contact_card_query( + &self, + request: QueryRequest, + access_token: &AccessToken, + ) -> impl Future> + Send; +} + +impl ContactCardQuery for Server { + async fn contact_card_query( + &self, + mut request: QueryRequest, + access_token: &AccessToken, + ) -> trc::Result { + todo!() + } +} diff --git a/crates/jmap/src/contact/set.rs b/crates/jmap/src/contact/set.rs new file mode 100644 index 00000000..04235a61 --- /dev/null +++ b/crates/jmap/src/contact/set.rs @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{Server, auth::AccessToken}; +use http_proto::HttpSessionData; +use jmap_proto::{ + method::set::{SetRequest, SetResponse}, + object::contact::ContactCard, +}; + +pub trait ContactCardSet: Sync + Send { + fn contact_card_set( + &self, + request: SetRequest<'_, ContactCard>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future>> + Send; +} + +impl ContactCardSet for Server { + async fn contact_card_set( + &self, + mut request: SetRequest<'_, ContactCard>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> trc::Result> { + todo!() + } +} diff --git a/crates/jmap/src/email/parse.rs b/crates/jmap/src/email/parse.rs index c06b2de0..22fd065e 100644 --- a/crates/jmap/src/email/parse.rs +++ b/crates/jmap/src/email/parse.rs @@ -4,11 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{ + body::{ToBodyPart, TruncateBody}, + headers::HeaderToValue, +}; +use crate::blob::download::BlobDownload; use common::{Server, auth::AccessToken}; use email::message::index::PREVIEW_LENGTH; use jmap_proto::{ - method::parse::{ParseEmailRequest, ParseEmailResponse}, - object::email::EmailProperty, + method::parse::{ParseRequest, ParseResponse}, + object::email::{Email, EmailProperty}, request::IntoValid, }; use jmap_tools::{Key, Map, Value}; @@ -18,27 +23,20 @@ use mail_parser::{ use std::future::Future; use utils::map::vec_map::VecMap; -use crate::blob::download::BlobDownload; - -use super::{ - body::{ToBodyPart, TruncateBody}, - headers::HeaderToValue, -}; - pub trait EmailParse: Sync + Send { fn email_parse( &self, - request: ParseEmailRequest, + request: ParseRequest, access_token: &AccessToken, - ) -> impl Future> + Send; + ) -> impl Future>> + Send; } impl EmailParse for Server { async fn email_parse( &self, - request: ParseEmailRequest, + request: ParseRequest, access_token: &AccessToken, - ) -> trc::Result { + ) -> trc::Result> { if request.blob_ids.len() > self.core.jmap.mail_parse_max_items { return Err(trc::JmapEvent::RequestTooLarge.into_err()); } @@ -70,6 +68,7 @@ impl EmailParse for Server { ] }); let body_properties = request + .arguments .body_properties .map(|v| v.into_valid().collect()) .unwrap_or_else(|| { @@ -86,12 +85,12 @@ impl EmailParse for Server { EmailProperty::Location, ] }); - let fetch_text_body_values = request.fetch_text_body_values.unwrap_or(false); - let fetch_html_body_values = request.fetch_html_body_values.unwrap_or(false); - let fetch_all_body_values = request.fetch_all_body_values.unwrap_or(false); - let max_body_value_bytes = request.max_body_value_bytes.unwrap_or(0); + let fetch_text_body_values = request.arguments.fetch_text_body_values.unwrap_or(false); + let fetch_html_body_values = request.arguments.fetch_html_body_values.unwrap_or(false); + let fetch_all_body_values = request.arguments.fetch_all_body_values.unwrap_or(false); + let max_body_value_bytes = request.arguments.max_body_value_bytes.unwrap_or(0); - let mut response = ParseEmailResponse { + let mut response = ParseResponse { account_id: request.account_id, parsed: VecMap::with_capacity(request.blob_ids.len()), not_parsable: vec![], diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index b4a19b5b..8e5f7403 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -24,9 +24,11 @@ use store::{ use trc::AddContext; use types::collection::Collection; +pub mod addressbook; pub mod api; pub mod blob; pub mod changes; +pub mod contact; pub mod email; pub mod identity; pub mod mailbox;