JMAP for Contacts implementation (part 1)

This commit is contained in:
mdecimus
2025-10-21 16:34:23 +02:00
parent b906eea62d
commit d0aef5d5a2
46 changed files with 1307 additions and 104 deletions
+2 -2
View File
@@ -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).
Generated
+2 -1
View File
@@ -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",
+12
View File
@@ -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",
}
}
}
+10
View File
@@ -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
)
}
+12
View File
@@ -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)
}
+1
View File
@@ -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"] }
+4
View File
@@ -9,6 +9,7 @@ use std::borrow::Cow;
use types::id::Id;
#[derive(Debug, Clone, serde::Serialize)]
#[serde(bound(serialize = "InvalidProperty<P>: serde::Serialize"))]
pub struct SetError<P: Property> {
#[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",
}
}
}
+13 -7
View File
@@ -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<K, V>(map: &AHashMap<K, V>) -> bool {
map.is_empty()
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(transparent)]
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct PropertyWrapper<T>(pub T)
where
T: serde::Serialize + Property;
pub struct PropertyWrapper<T: Property>(pub T);
impl<T: Property + serde::Serialize> From<T> for PropertyWrapper<T> {
impl<T: Property> From<T> for PropertyWrapper<T> {
fn from(value: T) -> Self {
Self(value)
}
}
impl<T: Property> Serialize for PropertyWrapper<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.0.to_cow().as_ref())
}
}
pub(crate) struct JmapDict<T: FromStr>(pub Vec<T>);
struct JmapDictVisitor<'de, T: FromStr> {
+21 -29
View File
@@ -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<T: JmapObject> {
pub account_id: Id,
pub blob_ids: Vec<MaybeInvalid<BlobId>>,
pub properties: Option<Vec<MaybeInvalid<EmailProperty>>>,
pub body_properties: Option<Vec<MaybeInvalid<EmailProperty>>>,
pub fetch_text_body_values: Option<bool>,
pub fetch_html_body_values: Option<bool>,
pub fetch_all_body_values: Option<bool>,
pub max_body_value_bytes: Option<usize>,
pub properties: Option<Vec<MaybeInvalid<T::Property>>>,
pub arguments: T::ParseArguments,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ParseEmailResponse {
pub struct ParseResponse<T: JmapObject> {
#[serde(rename = "accountId")]
pub account_id: Id,
#[serde(rename = "parsed")]
#[serde(skip_serializing_if = "VecMap::is_empty")]
pub parsed: VecMap<BlobId, Value<'static, EmailProperty, EmailValue>>,
pub parsed: VecMap<BlobId, Value<'static, T::Property, T::Element>>,
#[serde(rename = "notParsable")]
#[serde(skip_serializing_if = "Vec::is_empty")]
@@ -46,7 +42,7 @@ pub struct ParseEmailResponse {
pub not_found: Vec<BlobId>,
}
impl<'de> DeserializeArguments<'de> for ParseEmailRequest {
impl<'de, T: JmapObject> DeserializeArguments<'de> for ParseRequest<T> {
fn deserialize_argument<A>(&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::<serde::de::IgnoredAny>()?;
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<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -93,3 +74,14 @@ impl<'de> Deserialize<'de> for ParseEmailRequest {
deserialize_request(deserializer)
}
}
impl<T: JmapObject> Default for ParseRequest<T> {
fn default() -> Self {
Self {
account_id: Id::default(),
blob_ids: Vec::default(),
properties: None,
arguments: T::ParseArguments::default(),
}
}
}
+333
View File
@@ -0,0 +1,333 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<AddressBookProperty>),
}
#[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<Self> {
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<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
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<Self> {
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<bool>,
pub on_success_set_is_default: Option<Id>,
}
impl<'de> DeserializeArguments<'de> for AddressBookSetArguments {
fn deserialize_argument<A>(&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::<serde::de::IgnoredAny>()?;
}
);
Ok(())
}
}
impl FromStr for AddressBookProperty {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Id> for AddressBookProperty {
fn from(id: Id) -> Self {
AddressBookProperty::IdValue(id)
}
}
impl TryFrom<AddressBookProperty> for Id {
type Error = ();
fn try_from(value: AddressBookProperty) -> Result<Self, Self::Error> {
if let AddressBookProperty::IdValue(id) = value {
Ok(id)
} else {
Err(())
}
}
}
impl TryFrom<AddressBookProperty> for AddressBookRight {
type Error = ();
fn try_from(value: AddressBookProperty) -> Result<Self, Self::Error> {
if let AddressBookProperty::Rights(right) = value {
Ok(right)
} else {
Err(())
}
}
}
impl From<Id> for AddressBookValue {
fn from(id: Id) -> Self {
AddressBookValue::Id(id)
}
}
impl JmapObjectId for AddressBookValue {
fn as_id(&self) -> Option<Id> {
if let AddressBookValue::Id(id) = self {
Some(*id)
} else {
None
}
}
fn as_any_id(&self) -> Option<AnyId> {
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<AnyId> for AddressBookValue {
type Error = ();
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
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<AddressBookRight> for AddressBookProperty {
fn from(right: AddressBookRight) -> Self {
AddressBookProperty::Rights(right)
}
}
+2
View File
@@ -183,6 +183,8 @@ impl JmapObject for Blob {
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = BlobProperty::Id;
}
+230
View File
@@ -0,0 +1,230 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<Id>;
type Element = JSContactValue<Id, BlobId>;
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<Id, BlobId> {
fn as_id(&self) -> Option<Id> {
if let JSContactValue::Id(id) = self {
Some(*id)
} else {
None
}
}
fn as_any_id(&self) -> Option<AnyId> {
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<AnyId> for JSContactValue<Id, BlobId> {
type Error = ();
fn try_from(value: AnyId) -> Result<Self, Self::Error> {
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<A>(&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::<serde::de::IgnoredAny>()?;
}
);
Ok(())
}
}
impl<'de> DeserializeArguments<'de> for ContactComparator {
fn deserialize_argument<A>(&mut self, key: &str, map: &mut A) -> Result<(), A::Error>
where
A: serde::de::MapAccess<'de>,
{
if key == "property" {
let value = map.next_value::<Cow<str>>()?;
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::<serde::de::IgnoredAny>()?;
}
Ok(())
}
}
impl Default for ContactFilter {
fn default() -> Self {
ContactFilter::_T(String::new())
}
}
impl Default for ContactComparator {
fn default() -> Self {
ContactComparator::_T(String::new())
}
}
+41
View File
@@ -396,6 +396,15 @@ pub struct EmailQueryArguments {
pub collapse_threads: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct EmailParseArguments {
pub body_properties: Option<Vec<MaybeInvalid<EmailProperty>>>,
pub fetch_text_body_values: Option<bool>,
pub fetch_html_body_values: Option<bool>,
pub fetch_all_body_values: Option<bool>,
pub max_body_value_bytes: Option<usize>,
}
impl<'de> DeserializeArguments<'de> for EmailGetArguments {
fn deserialize_argument<A>(&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<A>(&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::<serde::de::IgnoredAny>()?;
}
);
Ok(())
}
}
impl serde::Serialize for EmailProperty {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -469,6 +508,8 @@ impl JmapObject for Email {
type CopyArguments = ();
type ParseArguments = EmailParseArguments;
const ID_PROPERTY: Self::Property = EmailProperty::Id;
}
@@ -330,6 +330,8 @@ impl JmapObject for EmailSubmission {
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = EmailSubmissionProperty::Id;
}
+3 -2
View File
@@ -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;
}
+5 -5
View File
@@ -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;
}
+5 -1
View File
@@ -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<Property = Self::Property>
+ From<Self::Id>
+ 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;
}
@@ -156,6 +156,8 @@ impl JmapObject for Principal {
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = PrincipalProperty::Id;
}
@@ -162,6 +162,8 @@ impl JmapObject for PushSubscription {
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = PushSubscriptionProperty::Id;
}
+2
View File
@@ -133,6 +133,8 @@ impl JmapObject for Quota {
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = QuotaProperty::Id;
}
+2
View File
@@ -152,6 +152,8 @@ impl JmapObject for Sieve {
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = SieveProperty::Id;
}
+2
View File
@@ -101,6 +101,8 @@ impl JmapObject for Thread {
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = ThreadProperty::Id;
}
@@ -130,6 +130,8 @@ impl JmapObject for VacationResponse {
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = VacationResponseProperty::Id;
}
+12
View File
@@ -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) => {
+1 -1
View File
@@ -136,7 +136,7 @@ impl ResponsePtr for BlobId {
}
}
impl<T: Property + serde::Serialize> ResponsePtr for PropertyWrapper<T> {
impl<T: Property> ResponsePtr for PropertyWrapper<T> {
fn eval_jptr(&self, _: JsonPointerIter<'_, Null>, results: &mut EvalResults) -> bool {
results.0.push(EvalResult::Property(self.0.to_cow()));
true
@@ -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)?,
@@ -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<usize>,
#[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<Id>,
}
#[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
)
}
}
+28
View File
@@ -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",
})
}
}
+19 -5
View File
@@ -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<Principal>),
Quota(GetRequest<Quota>),
Blob(GetRequest<Blob>),
AddressBook(GetRequest<AddressBook>),
ContactCard(GetRequest<ContactCard>),
}
#[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<Sieve>),
Principal(QueryRequest<Principal>),
Quota(QueryRequest<Quota>),
ContactCard(QueryRequest<ContactCard>),
}
#[derive(Debug)]
@@ -120,6 +127,13 @@ pub enum QueryChangesRequestMethod {
Sieve(QueryChangesRequest<Sieve>),
Principal(QueryChangesRequest<Principal>),
Quota(QueryChangesRequest<Quota>),
ContactCard(QueryChangesRequest<ContactCard>),
}
#[derive(Debug)]
pub enum ParseRequestMethod {
Email(ParseRequest<Email>),
ContactCard(ParseRequest<ContactCard>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
+61 -3
View File
@@ -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));
+65 -12
View File
@@ -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<Principal>),
Quota(GetResponse<Quota>),
Blob(GetResponse<Blob>),
AddressBook(GetResponse<AddressBook>),
ContactCard(GetResponse<ContactCard>),
}
#[derive(Debug, serde::Serialize)]
@@ -79,6 +82,8 @@ pub enum SetResponseMethod {
PushSubscription(SetResponse<PushSubscription>),
Sieve(SetResponse<Sieve>),
VacationResponse(SetResponse<VacationResponse>),
AddressBook(SetResponse<AddressBook>),
ContactCard(SetResponse<ContactCard>),
}
#[derive(Debug, serde::Serialize)]
@@ -90,15 +95,25 @@ pub enum ChangesResponseMethod {
Identity(ChangesResponse<Identity>),
EmailSubmission(ChangesResponse<EmailSubmission>),
Quota(ChangesResponse<Quota>),
AddressBook(ChangesResponse<AddressBook>),
ContactCard(ChangesResponse<ContactCard>),
}
#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub enum CopyResponseMethod {
Email(CopyResponse<Email>),
ContactCard(CopyResponse<ContactCard>),
Blob(CopyBlobResponse),
}
#[derive(Debug, serde::Serialize)]
#[serde(untagged)]
pub enum ParseResponseMethod {
Email(ParseResponse<Email>),
ContactCard(ParseResponse<ContactCard>),
}
#[derive(Debug, serde::Serialize)]
pub struct Response<'x> {
#[serde(rename = "methodResponses")]
@@ -229,7 +244,18 @@ impl<'x> From<GetResponse<Blob>> for ResponseMethod<'x> {
}
}
// Direct SetResponse conversions to ResponseMethod
impl<'x> From<GetResponse<ContactCard>> for ResponseMethod<'x> {
fn from(value: GetResponse<ContactCard>) -> Self {
ResponseMethod::Get(GetResponseMethod::ContactCard(value))
}
}
impl<'x> From<GetResponse<AddressBook>> for ResponseMethod<'x> {
fn from(value: GetResponse<AddressBook>) -> Self {
ResponseMethod::Get(GetResponseMethod::AddressBook(value))
}
}
impl<'x> From<SetResponse<Email>> for ResponseMethod<'x> {
fn from(value: SetResponse<Email>) -> Self {
ResponseMethod::Set(SetResponseMethod::Email(value))
@@ -272,7 +298,18 @@ impl<'x> From<SetResponse<VacationResponse>> for ResponseMethod<'x> {
}
}
// Direct ChangesResponse conversions to ResponseMethod
impl<'x> From<SetResponse<AddressBook>> for ResponseMethod<'x> {
fn from(value: SetResponse<AddressBook>) -> Self {
ResponseMethod::Set(SetResponseMethod::AddressBook(value))
}
}
impl<'x> From<SetResponse<ContactCard>> for ResponseMethod<'x> {
fn from(value: SetResponse<ContactCard>) -> Self {
ResponseMethod::Set(SetResponseMethod::ContactCard(value))
}
}
impl<'x> From<ChangesResponse<Email>> for ResponseMethod<'x> {
fn from(value: ChangesResponse<Email>) -> Self {
ResponseMethod::Changes(ChangesResponseMethod::Email(value))
@@ -309,7 +346,12 @@ impl<'x> From<ChangesResponse<Quota>> for ResponseMethod<'x> {
}
}
// Direct CopyResponse conversions to ResponseMethod
impl<'x> From<ChangesResponse<AddressBook>> for ResponseMethod<'x> {
fn from(value: ChangesResponse<AddressBook>) -> Self {
ResponseMethod::Changes(ChangesResponseMethod::AddressBook(value))
}
}
impl<'x> From<CopyResponse<Email>> for ResponseMethod<'x> {
fn from(value: CopyResponse<Email>) -> Self {
ResponseMethod::Copy(CopyResponseMethod::Email(value))
@@ -322,16 +364,27 @@ impl<'x> From<CopyBlobResponse> for ResponseMethod<'x> {
}
}
// Other direct conversions
impl<'x> From<CopyResponse<ContactCard>> for ResponseMethod<'x> {
fn from(value: CopyResponse<ContactCard>) -> Self {
ResponseMethod::Copy(CopyResponseMethod::ContactCard(value))
}
}
impl<'x> From<ImportEmailResponse> for ResponseMethod<'x> {
fn from(value: ImportEmailResponse) -> Self {
ResponseMethod::ImportEmail(value)
}
}
impl<'x> From<ParseEmailResponse> for ResponseMethod<'x> {
fn from(value: ParseEmailResponse) -> Self {
ResponseMethod::ParseEmail(value)
impl<'x> From<ParseResponse<Email>> for ResponseMethod<'x> {
fn from(value: ParseResponse<Email>) -> Self {
ResponseMethod::Parse(ParseResponseMethod::Email(value))
}
}
impl<'x> From<ParseResponse<ContactCard>> for ResponseMethod<'x> {
fn from(value: ParseResponse<ContactCard>) -> Self {
ResponseMethod::Parse(ParseResponseMethod::ContactCard(value))
}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<AddressBook>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<GetResponse<AddressBook>>> + Send;
}
impl AddressBookGet for Server {
async fn address_book_get(
&self,
mut request: GetRequest<AddressBook>,
access_token: &AccessToken,
) -> trc::Result<GetResponse<AddressBook>> {
todo!()
}
}
+8
View File
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod get;
pub mod set;
+32
View File
@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<Output = trc::Result<SetResponse<AddressBook>>> + Send;
}
impl AddressBookSet for Server {
async fn address_book_set(
&self,
mut request: SetRequest<'_, AddressBook>,
access_token: &AccessToken,
session: &HttpSessionData,
) -> trc::Result<SetResponse<AddressBook>> {
todo!()
}
}
+18 -4
View File
@@ -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,
+72 -11
View File
@@ -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);
+5 -1
View File
@@ -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!(),
})
}
}
+26 -2
View File
@@ -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());
}
+35
View File
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<Call<RequestMethod<'x>>>,
session: &HttpSessionData,
) -> impl Future<Output = trc::Result<CopyResponse<ContactCard>>> + Send;
}
impl JmapContactCardCopy for Server {
async fn contact_card_copy<'x>(
&self,
request: CopyRequest<'x, ContactCard>,
access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod<'x>>>,
session: &HttpSessionData,
) -> trc::Result<CopyResponse<ContactCard>> {
todo!()
}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<ContactCard>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<GetResponse<ContactCard>>> + Send;
}
impl ContactCardGet for Server {
async fn contact_card_get(
&self,
mut request: GetRequest<ContactCard>,
access_token: &AccessToken,
) -> trc::Result<GetResponse<ContactCard>> {
todo!()
}
}
+11
View File
@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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;
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<ContactCard>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<ParseResponse<ContactCard>>> + Send;
}
impl ContactCardParse for Server {
async fn contact_card_parse(
&self,
request: ParseRequest<ContactCard>,
access_token: &AccessToken,
) -> trc::Result<ParseResponse<ContactCard>> {
todo!()
}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<ContactCard>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
}
impl ContactCardQuery for Server {
async fn contact_card_query(
&self,
mut request: QueryRequest<ContactCard>,
access_token: &AccessToken,
) -> trc::Result<QueryResponse> {
todo!()
}
}
+32
View File
@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<Output = trc::Result<SetResponse<ContactCard>>> + Send;
}
impl ContactCardSet for Server {
async fn contact_card_set(
&self,
mut request: SetRequest<'_, ContactCard>,
access_token: &AccessToken,
session: &HttpSessionData,
) -> trc::Result<SetResponse<ContactCard>> {
todo!()
}
}
+17 -18
View File
@@ -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<Email>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<ParseEmailResponse>> + Send;
) -> impl Future<Output = trc::Result<ParseResponse<Email>>> + Send;
}
impl EmailParse for Server {
async fn email_parse(
&self,
request: ParseEmailRequest,
request: ParseRequest<Email>,
access_token: &AccessToken,
) -> trc::Result<ParseEmailResponse> {
) -> trc::Result<ParseResponse<Email>> {
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![],
+2
View File
@@ -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;