JMAP for Contacts implementation (part 1)

This commit is contained in:
mdecimus
2025-10-01 17:21:56 +02:00
parent b906eea62d
commit d0aef5d5a2
46 changed files with 1307 additions and 104 deletions

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!()
}
}

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;

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!()
}
}

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,

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);

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!(),
})
}
}

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());
}

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!()
}
}

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!()
}
}

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;

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!()
}
}

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!()
}
}

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!()
}
}

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![],

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;