🏡 index : ~doyle/jogre.git

author Jordan Doyle <jordan@doyle.la> 2023-09-28 2:15:48.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-09-28 2:25:46.0 +01:00:00
commit
2d9b8082946fd808932afe89340092c88f907495 [patch]
tree
1b3738c49f8d5e172f9ba2edcb667bcbbdb559a7
parent
0e2e088353d3a30bb3dc31482b871b8ef5c942d7
download
2d9b8082946fd808932afe89340092c88f907495.tar.gz

Foundational work for generic extension handling



Diff

 Cargo.lock                                       |  24 ++++++++++++++++++++++++
 jmap-proto/Cargo.toml                            |   1 +
 jogre-server/Cargo.toml                          |   1 +
 jmap-proto/src/errors.rs                         |  30 +++++++++++++++++++++++++-----
 jmap-proto/src/lib.rs                            |   2 ++
 jogre-server/src/context.rs                      |  16 ++++++++++++++++
 jogre-server/src/main.rs                         |   1 +
 jmap-proto/src/endpoints/mod.rs                  |  49 ++++++++++++++++++++++++++++++++++++-------------
 jmap-proto/src/endpoints/session.rs              |  14 +++++---------
 jmap-proto/src/extensions/js_contact.rs          | 640 --------------------------------------------------------------------------------
 jmap-proto/src/extensions/mod.rs                 |   3 ++-
 jmap-proto/src/extensions/sharing.rs             | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 jogre-server/src/context/oauth2.rs               |   1 -
 jogre-server/src/extensions/contacts.rs          |  42 ++++++++++++++++++++++++++++++++++++++++++
 jogre-server/src/extensions/core.rs              |  35 +++++++++++++++++++++++++++++++++++
 jogre-server/src/extensions/mod.rs               | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 jogre-server/src/extensions/sharing.rs           |  67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 jogre-server/src/methods/mod.rs                  |   7 ++++++-
 jogre-server/src/methods/session.rs              |  21 +++++----------------
 jmap-proto/src/extensions/contacts/js_contact.rs | 640 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 jmap-proto/src/extensions/contacts/mod.rs        |   1 +
 jogre-server/src/methods/api/mod.rs              | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 22 files changed, 1304 insertions(+), 686 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 350a0c1..ce94e31 100644
--- a/Cargo.lock
+++ a/Cargo.lock
@@ -904,6 +904,7 @@
 "serde",
 "serde_json",
 "serde_with",
 "strum",
]

[[package]]
@@ -935,6 +936,7 @@
 "rand",
 "rocksdb",
 "serde",
 "serde_json",
 "sha3",
 "tokio",
 "toml",
@@ -1651,6 +1653,28 @@
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"

[[package]]
name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
 "strum_macros",
]

[[package]]
name = "strum_macros"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "rustversion",
 "syn",
]

[[package]]
name = "subtle"
diff --git a/jmap-proto/Cargo.toml b/jmap-proto/Cargo.toml
index cdf8f8d..12558a8 100644
--- a/jmap-proto/Cargo.toml
+++ a/jmap-proto/Cargo.toml
@@ -10,3 +10,4 @@
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_with = { version = "3.3", features = ["macros"] }
strum = { version = "0.25", features = ["derive"] }
diff --git a/jogre-server/Cargo.toml b/jogre-server/Cargo.toml
index 1bf6862..cea0972 100644
--- a/jogre-server/Cargo.toml
+++ a/jogre-server/Cargo.toml
@@ -31,4 +31,5 @@
url = { version = "2.4", features = ["serde"] }
uuid = { version = "1.4", features = ["v4", "serde"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0"
sha3 = "0.10"
diff --git a/jmap-proto/src/errors.rs b/jmap-proto/src/errors.rs
index 97c1e69..f0555c9 100644
--- a/jmap-proto/src/errors.rs
+++ a/jmap-proto/src/errors.rs
@@ -1,16 +1,19 @@
use std::{borrow::Cow, collections::HashMap};

use serde::{Deserialize, Serialize};
use serde_json::Value;
use strum::Display;

use crate::endpoints::{Argument, Arguments, Invocation};

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RequestError {
    #[serde(rename = "type")]
    type_: ProblemType,
    status: u16,
    detail: Cow<'static, str>,
    pub type_: ProblemType,
    pub status: u16,
    pub detail: Cow<'static, str>,
    #[serde(flatten)]
    meta: HashMap<String, Value>,
    pub meta: HashMap<String, Value>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -44,7 +47,8 @@
/// Any further method calls in the request MUST then be processed as

/// normal.  Errors at the method level MUST NOT generate an HTTP-level

/// error.

#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Display)]
#[serde(tag = "type")]
pub enum MethodError {
    /// Some internal server resource was temporarily unavailable.

    ///

@@ -86,4 +90,20 @@
    /// This method modifies state, but the account is read-only (as returned on

    /// the corresponding Account object in the JMAP Session resource).

    AccountReadOnly,
}

impl MethodError {
    pub fn into_invocation(self, request_id: Cow<'_, str>) -> Invocation<'_> {
        let mut arguments = Arguments::default();
        arguments.0.insert(
            Cow::Borrowed("type"),
            Argument::Absolute(Value::String(self.to_string())),
        );

        Invocation {
            name: "error".into(),
            arguments,
            request_id,
        }
    }
}
diff --git a/jmap-proto/src/lib.rs b/jmap-proto/src/lib.rs
index f89ca07..07b6dc0 100644
--- a/jmap-proto/src/lib.rs
+++ a/jmap-proto/src/lib.rs
@@ -1,6 +1,8 @@
pub mod common;
pub mod endpoints;
pub mod errors;
pub mod events;
pub mod extensions;
pub(crate) mod util;

pub use serde_json::Value;
diff --git a/jogre-server/src/context.rs b/jogre-server/src/context.rs
index bbf431a..0cde0da 100644
--- a/jogre-server/src/context.rs
+++ a/jogre-server/src/context.rs
@@ -1,7 +1,12 @@
use std::sync::Arc;

use crate::{
    config::{Config, CoreCapabilities},
    extensions,
    extensions::{
        sharing::{Principals, PrincipalsOwner},
        ExtensionRegistry,
    },
    store::Store,
};

@@ -12,6 +17,7 @@
    pub store: Arc<Store>,
    pub base_url: url::Url,
    pub core_capabilities: CoreCapabilities,
    pub extension_registry: ExtensionRegistry,
}

impl Context {
@@ -19,11 +25,21 @@
        let derived_keys = Arc::new(DerivedKeys::new(&config.private_key));
        let store = Arc::new(Store::from_config(config.store));

        let extension_registry = ExtensionRegistry {
            core: extensions::core::Core {
                core_capabilities: config.core_capabilities,
            },
            contacts: extensions::contacts::Contacts {},
            sharing_principals: Principals {},
            sharing_principals_owner: PrincipalsOwner {},
        };

        Self {
            oauth2: oauth2::OAuth2::new(store.clone(), derived_keys),
            store,
            base_url: config.base_url,
            core_capabilities: config.core_capabilities,
            extension_registry,
        }
    }
}
diff --git a/jogre-server/src/main.rs b/jogre-server/src/main.rs
index 2dd4f5b..f212c5e 100644
--- a/jogre-server/src/main.rs
+++ a/jogre-server/src/main.rs
@@ -1,8 +1,9 @@
#![deny(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]

mod config;
mod context;
mod extensions;
mod layers;
mod methods;
mod store;
diff --git a/jmap-proto/src/endpoints/mod.rs b/jmap-proto/src/endpoints/mod.rs
index be81b4f..8670dc7 100644
--- a/jmap-proto/src/endpoints/mod.rs
+++ a/jmap-proto/src/endpoints/mod.rs
@@ -27,8 +27,31 @@
const REFERENCE_OCTOTHORPE: &str = "#";

#[derive(Debug, Clone, Default)]
pub struct Arguments<'a>(HashMap<Cow<'a, str>, Argument<'a>>);
pub struct Arguments<'a>(pub HashMap<Cow<'a, str>, Argument<'a>>);

impl Arguments<'_> {
    /// Resolves a pointer, as defined in [RFC 6901]

    ///

    /// [RFC 6901]: https://datatracker.ietf.org/doc/html/rfc6901

    pub fn pointer(&self, pointer: &str) -> Option<Cow<Value>> {
        if pointer.is_empty() {
            return Some(Cow::Owned(serde_json::to_value(self).unwrap()));
        }

        let pointer = pointer.strip_prefix('/')?;

        let mut pointer = pointer.splitn(2, pointer);

        if let Argument::Absolute(value) = self.0.get(pointer.next()?)? {
            value
                .pointer(pointer.next().unwrap_or(""))
                .map(Cow::Borrowed)
        } else {
            None
        }
    }
}

impl<'a> Serialize for Arguments<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
@@ -101,15 +124,15 @@
    /// The method call id (see Section 3.2) of a previous method call in

    /// the current request.

    #[serde(borrow)]
    result_of: Cow<'a, str>,
    pub result_of: Cow<'a, str>,
    /// The required name of a response to that method call.

    #[serde(borrow)]
    name: Cow<'a, str>,
    pub name: Cow<'a, str>,
    /// A pointer into the arguments of the response selected via the name

    /// and resultOf properties.  This is a JSON Pointer [RFC6901], except

    /// it also allows the use of "*" to map through an array.

    #[serde(borrow)]
    path: Cow<'a, str>,
    pub path: Cow<'a, str>,
}

/// Method calls and responses are represented by the *Invocation* data

@@ -118,16 +141,16 @@
#[derive(Clone, Debug)]
pub struct Invocation<'a> {
    /// A "String" *name* of the method to call or of the response.

    name: Cow<'a, str>,
    pub name: Cow<'a, str>,
    /// A "String[*]" object containing named *arguments* for that method

    /// or response.

    arguments: Arguments<'a>,
    pub arguments: Arguments<'a>,
    /// A "String" *method call id*: an arbitrary string from the client

    /// to be echoed back with the responses emitted by that method call

    /// (a method may return 1 or more responses, as it may make implicit

    /// calls to other methods; all responses initiated by this method

    /// call get the same method call id in the response).

    request_id: Cow<'a, str>,
    pub request_id: Cow<'a, str>,
}

impl<'a> Serialize for Invocation<'a> {
@@ -195,11 +218,11 @@
    /// of specifications it supports in the Session object (see

    /// Section 2), as keys on the "capabilities" property.

    #[serde_as(as = "Vec<BorrowedCow>")]
    using: Vec<Cow<'a, str>>,
    pub using: Vec<Cow<'a, str>>,
    /// An array of method calls to process on the server.  The method

    /// calls MUST be processed sequentially, in order.

    #[serde(borrow)]
    method_calls: Vec<Invocation<'a>>,
    pub method_calls: Vec<Invocation<'a>>,
    /// A map of a (client-specified) creation id to the id the server

    /// assigned when a record was successfully created.

    ///

@@ -210,7 +233,7 @@
    /// specify the creation id it assigned, prefixed with a "#" (see

    /// Section 5.3 for more details).

    #[serde(borrow)]
    created_ids: Option<HashMap<Id<'a>, Id<'a>>>,
    pub created_ids: Option<HashMap<Id<'a>, Id<'a>>>,
}

#[serde_as]
@@ -222,16 +245,16 @@
    /// the "methodResponses" array in the same order that the methods are

    /// processed.

    #[serde(borrow)]
    method_responses: Invocation<'a>,
    pub method_responses: Vec<Invocation<'a>>,
    /// A map of a (client-specified) creation id to the id the server

    /// assigned when a record was successfully created.  This MUST

    /// include all creation ids passed in the original createdIds

    /// parameter of the Request object, as well as any additional ones

    /// added for newly created records.

    #[serde(borrow)]
    created_ids: Option<HashMap<Id<'a>, Id<'a>>>,
    pub created_ids: Option<HashMap<Id<'a>, Id<'a>>>,
    /// The current value of the "state" string on the Session object, as

    /// described in Section 2.  Clients may use this to detect if this

    /// object has changed and needs to be refetched.

    session_state: SessionState<'a>,
    pub session_state: SessionState<'a>,
}
diff --git a/jmap-proto/src/endpoints/session.rs b/jmap-proto/src/endpoints/session.rs
index 01d3725..5be1d96 100644
--- a/jmap-proto/src/endpoints/session.rs
+++ a/jmap-proto/src/endpoints/session.rs
@@ -1,9 +1,10 @@
use std::{
    borrow::Cow,
    collections::{BTreeSet, HashMap},
};

use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::{serde_as, BorrowCow};

use crate::common::{Id, SessionState, UnsignedInt};
@@ -24,8 +25,11 @@
    /// a URI for a capability supported by the server.  The value for

    /// each of these keys is an object with further information about the

    /// server's capabilities in relation to that capability.

    ///

    /// The capabilities object MUST include a property called

    /// "urn:ietf:params:jmap:core".

    #[serde(borrow)]
    pub capabilities: ServerCapabilities<'a>,
    pub capabilities: HashMap<Cow<'a, str>, Value>,
    /// A map of an account id to an Account object for each account (see

    /// Section 1.6.2) the user has access to.

    #[serde(borrow)]
@@ -76,14 +80,6 @@
    /// need to refetch the object.

    #[serde(borrow)]
    pub state: SessionState<'a>,
}

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ServerCapabilities<'a> {
    /// The capabilities object MUST include a property called

    /// "urn:ietf:params:jmap:core".

    #[serde(rename = "urn:ietf:params:jmap:core", borrow)]
    pub core: CoreCapability<'a>,
}

#[serde_as]
diff --git a/jmap-proto/src/extensions/js_contact.rs b/jmap-proto/src/extensions/js_contact.rs
deleted file mode 100644
index 56646be..0000000 100644
--- a/jmap-proto/src/extensions/js_contact.rs
+++ /dev/null
@@ -1,640 +1,0 @@
use std::{borrow::Cow, collections::HashMap};

use chrono::NaiveDate;
use serde::{
    ser::SerializeMap, Deserialize, Serialize, Serializer, __private::ser::FlatMapSerializer,
};
use serde_json::Value;

use crate::common::{Id, UnsignedInt, UtcDate};

#[derive(Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct TypeWrapper<T>(T);

impl<T: Serialize + TypedStruct> Serialize for TypeWrapper<T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("@type", T::KIND)?;
        self.0.serialize(FlatMapSerializer(&mut map))?;
        map.end()
    }
}

pub trait TypedStruct {
    const KIND: &'static str;
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase", tag = "@type")]
pub enum Data<'a> {
    Card(#[serde(borrow)] Card<'a>),
    CardGroup(CardGroup<'a>),
}

/// A CardGroup object represents a group of cards. Its members may be Cards or CardGroups.

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CardGroup<'a> {
    /// An identifier, used to associate the object as the same across different

    /// systems, addressbooks and views.

    #[serde(borrow)]
    uid: Id<'a>,
    /// The set is represented as an object, with each key being the uid of another Card or

    /// CardGroup. The value for each key in the object MUST be true.

    members: HashMap<Id<'a>, bool>,
    /// The user-visible name for the group, e.g. "Friends". This may be any UTF-8 string of at

    /// least 1 character in length and maximum 255 octets in size. The same name may be used by

    /// two different groups.

    #[serde(default)]
    name: Cow<'a, str>,
    /// The card that represents this group.

    card: Option<Card<'a>>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Card<'a> {
    /// An identifier, used to associate the object as the same across different

    /// systems, addressbooks and views.

    #[serde(borrow)]
    uid: Id<'a>,
    /// The identifier for the product that created the Card object.

    prod_id: Option<Cow<'a, str>>,
    /// The date and time when this Card object was created.

    created: Option<UtcDate>,
    /// The date and time when the data in this Card object was last modified.

    updated: Option<UtcDate>,
    /// The kind of the entity the Card represents.

    kind: Option<CardKind>,
    /// Relates the object to other Card and CardGroup objects. This is

    /// represented as a map, where each key is the

    #[serde(default)]
    related_to: HashMap<Id<'a>, TypeWrapper<Relation>>,
    /// Language used for free-form text on this card.

    language: Option<Cow<'a, str>>,
    /// The name components of the name of the entity represented by this Card.

    #[serde(default)]
    name: Vec<TypeWrapper<NameComponent<'a>>>,
    /// The full name (e.g. the personal name and surname of an individual, the

    /// name of an organization) of the entity represented by this card. The

    /// purpose of this property is to define a name, even if the individual

    /// name components are not known. In addition, it is meant to provide

    /// alternative versions of the name for internationalisation.

    ///

    /// Implementations SHOULD prefer using the name property over this one

    /// and SHOULD NOT store the concatenated name component values in this

    /// property.

    #[serde(default)]
    full_name: Cow<'a, str>,
    /// The nick names of the entity represented by this card.

    #[serde(default)]
    nick_names: Vec<Cow<'a, str>>,
    /// The companies or organization names and units associated with this

    /// card.

    #[serde(default)]
    organizations: HashMap<Id<'a>, TypeWrapper<Organization<'a>>>,
    /// The job titles or functional positions of the entity represented by

    /// this card.

    #[serde(default)]
    titles: HashMap<Id<'a>, TypeWrapper<Title<'a>>>,
    /// The email addresses to contact the entity represented by this card.

    #[serde(default)]
    emails: HashMap<Id<'a>, TypeWrapper<EmailAddress<'a>>>,
    /// The phone numbers to contact the entity represented by this card.

    #[serde(default)]
    phones: HashMap<Id<'a>, TypeWrapper<Phone<'a>>>,
    /// The online resources and services that are associated with the entity

    /// represented by this card.

    #[serde(default)]
    online: HashMap<Id<'a>, TypeWrapper<Resource<'a>>>,
    /// A map of photo ids to File objects that contain photographs or images

    /// associated with this card. A typical use case is to include an avatar for display along the

    /// contact name.

    #[serde(default)]
    photos: HashMap<Id<'a>, TypeWrapper<File<'a>>>,
    /// Defines the preferred method to contact the holder of this card.

    preferred_contact_method: Option<PreferredContactMethod>,
    /// Defines the preferred languages for contacting the entity associated with this card. The

    /// keys in the object MUST be [RFC5646] language tags. The values are a (possibly empty) list

    /// of contact language preferences for this language. A valid ContactLanguage object MUST have

    /// at least one of its properties set.

    #[serde(default)]
    preferred_contact_languages: HashMap<String, TypeWrapper<ContactLanguage>>,
    /// A map of address ids to Address objects, containing physical locations.

    #[serde(default)]
    address: HashMap<Id<'a>, TypeWrapper<Address<'a>>>,
    /// A map of language tags [RFC5646] to patches, which localize a property value into the

    /// locale of the respective language tag.

    ///

    /// A patch MUST NOT target the localizations property.

    #[serde(default)]
    localizations: HashMap<Cow<'a, str>, Value>,
    /// These are memorable dates and events for the entity represented by this card.

    #[serde(default)]
    anniversaries: HashMap<Id<'a>, TypeWrapper<Anniversary<'a>>>,
    /// Defines personal information about the entity represented by this card.

    #[serde(default)]
    personal_info: HashMap<Id<'a>, TypeWrapper<PersonalInfo<'a>>>,
    /// Arbitrary notes about the entity represented by this card.

    #[serde(default)]
    notes: Cow<'a, str>,
    /// The set of free-text or URI categories that relate to the card. The set is represented as

    /// an object, with each key being a category. The value for each key in the object MUST be

    /// true.

    #[serde(default)]
    categories: HashMap<Cow<'a, str>, bool>,
    ///  Maps identifiers of custom time zones to their time zone definitions. For a description of

    /// this property see the timeZones property definition in [RFC8984].

    #[serde(default)]
    time_zones: HashMap<Cow<'a, str>, Value>,
}

/// Defines personal information about the entity represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PersonalInfo<'a> {
    /// Specifies the type for this personal information.

    #[serde(rename = "type")]
    type_: PersonalInfoType,
    /// The actual information. This generally is free-text, but future

    /// specifications MAY restrict allowed values depending on the type of

    /// this PersonalInformation.

    value: Cow<'a, str>,
    /// Indicates the level of expertise, or engagement in hobby or interest.

    level: Option<PersonalInfoLevel>,
}

impl TypedStruct for PersonalInfo<'_> {
    const KIND: &'static str = "PersonalInfo";
}

/// Specifies the type for this personal information.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PersonalInfoType {
    /// A field of expertise or credential

    Expertise,
    /// A hobby

    Hobby,
    /// An interest

    Interest,
    /// An information not covered by the above categories

    Other,
}

/// Indicates the level of expertise, or engagement in hobby or interest.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PersonalInfoLevel {
    High,
    Medium,
    Low,
}

/// These are memorable dates and events for the entity represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Anniversary<'a> {
    /// Specifies the type of the anniversary.

    #[serde(rename = "type")]
    type_: AnniversaryType,
    /// A label describing the value in more detail, especially if the type

    /// property has value other (but MAY be included with any type).

    #[serde(default)]
    label: Cow<'a, str>,
    /// The date of this anniversary, in the form "YYYY-MM-DD"

    /// (any part may be all 0s for unknown) or a [RFC3339] timestamp.

    date: NaiveDate,
    /// An address associated with this anniversary, e.g. the place of birth or

    /// death.

    place: Option<Address<'a>>,
}

impl TypedStruct for Anniversary<'_> {
    const KIND: &'static str = "Anniversary";
}

/// Specifies the type of the anniversary.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum AnniversaryType {
    /// A birth day anniversary

    Birth,
    /// A death day anniversary

    Death,
    /// An anniversary not covered by any of the known types.

    Other,
}

/// A physical location.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Address<'a> {
    ///  The complete address, excluding type and label. This property is mainly useful to

    /// represent addresses of which the individual address components are unknown, or to provide

    /// localized representations.

    #[serde(default)]
    full_address: Cow<'a, str>,
    ///  The street address. The concatenation of the component values, separated by whitespace,

    /// SHOULD result in a valid street address for the address locale. Doing so, implementations

    /// MAY ignore any separator components. The StreetComponent object type is defined in the

    /// paragraph below.

    #[serde(default)]
    street: Vec<TypeWrapper<StreetComponent<'a>>>,
    /// The city, town, village, post town, or other locality within which the street address may

    /// be found.

    #[serde(default)]
    locality: Cow<'a, str>,
    /// The province, such as a state, county, or canton within which the locality may be found.

    #[serde(default)]
    region: Cow<'a, str>,
    /// The country name.

    #[serde(default)]
    country: Cow<'a, str>,
    /// The postal code, post code, ZIP code or other short code associated with the address by the

    /// relevant country's postal system.

    #[serde(default)]
    postcode: Cow<'a, str>,
    /// The ISO-3166-1 country code.

    #[serde(default)]
    country_code: Cow<'a, str>,
    /// A [RFC5870] "geo:" URI for the address.

    #[serde(default)]
    coordinates: Cow<'a, str>,
    /// Identifies the time zone this address is located in. This either MUST be a time zone name

    /// registered in the IANA Time Zone Database, or it MUST be a valid TimeZoneId as defined in

    /// [RFC8984]. For the latter, a corresponding time zone MUST be defined in the timeZones

    /// property.

    #[serde(default)]
    time_zone: Cow<'a, str>,
    /// The contexts of the address information.

    #[serde(default)]
    context: HashMap<AddressContext, bool>,
    /// A label describing the value in more detail.

    #[serde(default)]
    label: Cow<'a, str>,
    ///  The preference of this address in relation to other addresses.

    pref: Option<Preference>,
}

impl TypedStruct for Address<'_> {
    const KIND: &'static str = "Address";
}

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase", untagged)]
pub enum AddressContext {
    /// An address to be used for billing.

    Billing,
    /// An address to be used for delivering physical items

    Postal,
    /// A normal context

    Other(Context),
}

///  The street address. The concatenation of the component values, separated by whitespace, SHOULD

/// result in a valid street address for the address locale. Doing so, implementations MAY ignore

/// any separator components. The StreetComponent object type is defined in the paragraph below.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct StreetComponent<'a> {
    /// The type of this street component.

    #[serde(rename = "type")]
    type_: StreetComponentKind,
    /// The value of this street component.

    value: Cow<'a, str>,
}

impl TypedStruct for StreetComponent<'_> {
    const KIND: &'static str = "StreetComponent";
}

/// The type of this street component.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum StreetComponentKind {
    Name,
    Number,
    Apartment,
    Room,
    Extension,
    Direction,
    Building,
    Floor,
    PostOfficeBox,
    Separator,
    Unknown,
}

/// Defines the preferred method to contact the holder of this card.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ContactLanguage {
    /// Defines the context in which to use this language.

    context: Option<Context>,
    /// Defines the preference of this language in relation to other

    /// languages of the same context.

    pref: Option<Preference>,
}

impl TypedStruct for ContactLanguage {
    const KIND: &'static str = "ContactLanguage";
}

/// Defines the preferred method to contact the holder of this card.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PreferredContactMethod {
    Emails,
    Phones,
    Online,
}

/// A map of photo ids to File objects that contain photographs or images

/// associated with this card. A typical use case is to include an avatar for display along the

/// contact name.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct File<'a> {
    /// A URI where to fetch the data of this file.

    href: Cow<'a, str>,
    /// The content-type of the file, if known.

    media_type: Cow<'a, str>,
    /// The size, in octets, of the file when fully decoded (i.e., the number

    /// of octets in the file the user would download), if known.

    size: Option<UnsignedInt>,
    /// The preference of this photo in relation to other photos.

    pref: Option<Preference>,
}

impl TypedStruct for File<'_> {
    const KIND: &'static str = "File";
}

/// The online resources and services that are associated with the entity

/// represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Resource<'a> {
    /// resource value, where the allowed value form is defined by the the type

    /// property. In any case the value MUST NOT be empty.

    resource: Cow<'a, str>,
    /// The type of the resource value.

    #[serde(rename = "type")]
    type_: ResourceType,
    /// Used for URI resource values. Provides the media type [RFC2046] of the

    /// resource identified by the URI.

    #[serde(default)]
    media_type: Cow<'a, str>,
    /// The contexts in which to use this resource.

    #[serde(default)]
    context: HashMap<Context, bool>,
    /// A label describing the value in more detail, especially if the type

    /// property has value other (but MAY be included with any type).

    #[serde(default)]
    label: Cow<'a, str>,
    /// The preference of this resource in relation to other resources.

    pref: Option<Preference>,
}

impl TypedStruct for Resource<'_> {
    const KIND: &'static str = "Resource";
}

/// The type of the resource value.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ResourceType {
    /// The resource value is a URI, e.g. a website link. This MUST be a valid URI as defined in

    /// Section 3 of [RFC3986] and updates.

    Uri,
    /// The resource value is a username associated with the entity represented by this card (e.g.

    /// for social media, or an IM client). The label property SHOULD be included to identify what

    /// service this is for. For compatibility between clients, this label SHOULD be the canonical

    /// service name, including capitalisation. e.g. Twitter, Facebook, Skype, GitHub, XMPP. The

    /// resource value may be any non-empty free text.

    Username,
    /// The resource value is something else not covered by the above categories. A label property

    /// MAY be included to display next to the number to help the user identify its purpose. The

    /// resource value may be any non-empty free text.

    Other,
}

/// The phone numbers to contact the entity represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Phone<'a> {
    /// The phone value, as either a URI or a free-text phone number. Typical

    /// URI schemes are the [RFC3966] tel or [RFC3261] sip schemes, but any

    /// URI scheme is allowed.

    phone: Cow<'a, str>,
    ///  The set of contact features that this phone number may be used for. The

    /// set is represented as an object, with each key being a method type. The

    /// value for each key in the object MUST be true.

    #[serde(default)]
    features: HashMap<PhoneFeature, bool>,
    // The contexts in which to use this number. The value for each
    /// key in the object MUST be true.

    #[serde(default)]
    contexts: HashMap<Context, bool>,
    /// A label describing the value in more detail, especially if the type

    /// property has value other (but MAY be included with any type).

    #[serde(default)]
    label: Cow<'a, str>,
    /// The preference of this email address in relation to other email addresses.

    pref: Option<Preference>,
}

impl TypedStruct for Phone<'_> {
    const KIND: &'static str = "Phone";
}

/// The email addresses to contact the entity represented by this card.

#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum PhoneFeature {
    /// The number is for calling by voice.

    Voice,
    /// The number is for sending faxes.

    Fax,
    /// The number is for a pager or beeper.

    Pager,
    /// The number supports text messages (SMS).

    Text,
    /// The number is for a cell phone.

    Cell,
    /// The number is for a device for people with hearing or speech difficulties.

    Textphone,
    /// The number supports video conferencing.

    Video,
    /// The number is for some other purpose. The label property MAY be included

    /// to display next to the number to help the user identify its purpose.

    Other,
}

/// The email addresses to contact the entity represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct EmailAddress<'a> {
    /// The email address. This MUST be an addr-spec value as defined in

    /// Section 3.4.1 of [RFC5322].

    email: Cow<'a, str>,
    /// The contexts in which to use this email address. The value for each

    /// key in the object MUST be true.

    #[serde(default)]
    contexts: HashMap<Context, bool>,
    /// The preference of this email address in relation to other email addresses.

    pref: Option<Preference>,
}

impl TypedStruct for EmailAddress<'_> {
    const KIND: &'static str = "EmailAddress";
}

/// This data type allows to define a preference order on same-typed contact

/// information. For example, a card holder may have two email addresses and

/// prefer to be contacted with one of them.

///

/// A preference value MUST be an integer number in the range 1 and 100. Lower

/// values correspond to a higher level of preference, with 1 being most

/// preferred. If no preference is set, then the contact information MUST be

/// interpreted as being least preferred.

///

/// Note that the preference only is defined in relation to contact information

/// of the same type. For example, the preference orders within emails and

/// phone numbers are indendepent of each other. Also note that the

/// preferredContactMethod property allows to define a preferred contact method

/// across method types.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Preference(u8);

/// The companies or organization names and units associated with this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Title<'a> {
    /// The name of this organization.

    #[serde(borrow)]
    name: Cow<'a, str>,
    /// The id of the organization in which this title is held.

    #[serde(default)]
    organization: Vec<Id<'a>>,
}

impl TypedStruct for Title<'_> {
    const KIND: &'static str = "Title";
}

/// The companies or organization names and units associated with this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Organization<'a> {
    ///  The name of this organization.

    name: Cow<'a, str>,
    ///  Additional levels of organizational unit names.

    #[serde(default)]
    units: Vec<Cow<'a, str>>,
}

impl TypedStruct for Organization<'_> {
    const KIND: &'static str = "Organization";
}

/// The name components of the name of the entity represented by this Card. Name

/// components SHOULD be ordered such that their values joined by whitespace

/// produce a valid full name of this entity. Doing so, implementations MAY

/// ignore any separator components.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct NameComponent<'a> {
    value: Cow<'a, str>,
    #[serde(rename = "type")]
    type_: NameComponentKind,
}

impl TypedStruct for NameComponent<'_> {
    const KIND: &'static str = "NameComponent";
}

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum NameComponentKind {
    /// The value is a honorific title(s), e.g. "Mr", "Ms", "Dr".

    Prefix,
    /// The value is a personal name(s), also known as "first name", "given name".

    Personal,
    /// The value is a surname, also known as "last name", "family name".

    Surname,
    /// The value is an additional name, also known as "middle name".

    Additional,
    /// The value is a honorific suffix, e.g. "B.A.", "Esq.".

    Suffix,
    /// A separator for two name components. The value property of the component

    /// includes the verbatim separator, for example a newline character.

    Separator,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Relation {
    relation: HashMap<RelationKind, bool>,
}

impl TypedStruct for Relation {
    const KIND: &'static str = "Relation";
}

/// Contact information typically is associated with a context in which it

/// should be used. For example, someone might have distinct phone numbers

/// for work and private contexts. The Context data type enumerates common

/// contexts.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Context {
    /// The contact information may be used to contact the card holder in a

    /// private context.

    Private,
    /// The contact information may be used to contact the card holder in a

    /// professional context.

    Work,
    /// The contact information may be used to contact the card holder in some

    /// other context.

    Other,
}

#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum RelationKind {
    Contact,
    Acquaintance,
    Friend,
    Met,
    CoWorker,
    Colleague,
    CoResident,
    Neighbor,
    Child,
    Parent,
    Sibling,
    Spouse,
    Kin,
    Muse,
    Crush,
    Date,
    Sweetheart,
    Me,
    Agent,
    Emergency,
}

/// The kind of the entity the Card represents.

#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum CardKind {
    /// A single person

    Individual,
    /// An organization

    Org,
    /// A named location

    Location,
    /// A device, such as appliances, computers, or network elements

    Device,
    /// A software application

    Application,
}
diff --git a/jmap-proto/src/extensions/mod.rs b/jmap-proto/src/extensions/mod.rs
index 034ca9c..e5b1541 100644
--- a/jmap-proto/src/extensions/mod.rs
+++ a/jmap-proto/src/extensions/mod.rs
@@ -1,1 +1,2 @@
pub mod js_contact;
pub mod contacts;
pub mod sharing;
diff --git a/jmap-proto/src/extensions/sharing.rs b/jmap-proto/src/extensions/sharing.rs
new file mode 100644
index 0000000..5970ebe 100644
--- /dev/null
+++ a/jmap-proto/src/extensions/sharing.rs
@@ -1,0 +1,137 @@
use std::{borrow::Cow, collections::HashMap};

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{
    common::{Id, UtcDate},
    endpoints::session::Account,
};

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrincipalsSessionCapabilities {}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrincipalsAccountCapabilities<'a> {
    /// The id of the principal in this account that corresponds to the user

    /// fetching this object, if any.

    #[serde(borrow)]
    pub current_user_principal_id: Option<Id<'a>>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrincipalsOwnerAccountCapabilities<'a> {
    /// The id of an account with the `urn:ietf:params:jmap:principals`

    /// capability that contains the corresponding Principal object.

    #[serde(borrow)]
    pub account_id_for_principal: Id<'a>,
    /// The id of the Principal that owns this account.

    #[serde(borrow)]
    pub principal_id: Id<'a>,
}

/// A Principal represents an individual, group, location (e.g. a room),

/// resource (e.g. a projector) or other entity in a collaborative environment.

/// Sharing in JMAP is generally configured by assigning rights to certain data

/// within an account to other principals, for example a user may assign

/// permission to read their calendar to a principal representing another user,

/// or their team.

///

/// In a shared environment such as a workplace, a user may have access to a

/// large number of principals.

///

/// In most systems the user will have access to a single Account containing

/// Principal objects, but they may have access to multiple if, for example,

/// aggregating data from different places.

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Principal<'a> {
    /// The id of the principal.

    #[serde(borrow)]
    pub id: Id<'a>,
    pub type_: PrincipalType,
    /// The name of the principal, e.g. “Jane Doe”, or “Room 4B”.

    #[serde(borrow)]
    pub name: Cow<'a, str>,
    /// A longer description of the principal, for example details about the facilities of a

    /// resource, or null if no description available.

    #[serde(borrow)]
    pub description: Option<Cow<'a, str>>,
    /// An email address for the principal, or null if no email is available.

    #[serde(borrow)]
    pub email: Option<Cow<'a, str>>,
    /// The time zone for this principal, if known. If not null, the value MUST

    /// be a time zone id from the IANA Time Zone Database TZDB.

    #[serde(borrow)]
    pub time_zone: Option<Cow<'a, str>>,
    /// A map of JMAP capability URIs to domain specific information about the principal in

    /// relation to that capability, as defined in the document that registered the capability.

    #[serde(borrow)]
    pub capabilities: HashMap<Cow<'a, str>, Value>,
    /// A map of account id to Account object for each JMAP Account containing data for this

    /// principal that the user has access to, or null if none.

    #[serde(borrow)]
    pub accounts: Option<HashMap<Id<'a>, Account<'a>>>,
}

#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub enum PrincipalType {
    /// This represents a single person.

    Individual,
    /// This represents a group of people.

    Group,
    /// This represents some resource, e.g. a projector.

    Resource,
    /// This represents a location.

    Location,
    /// This represents some other undefined principal.

    Other,
}

/// The ShareNotification data type records when the user’s permissions to access a shared object

/// changes. ShareNotification are only created by the server; users cannot create them explicitly.

/// Notifications are stored in the same Account as the Principals.

///

/// Clients SHOULD present the list of notifications to the user and allow them to dismiss them. To

/// dismiss a notification you use a standard “/set” call to destroy it.

///

/// The server SHOULD create a ShareNotification whenever the user’s permissions change on an

/// object. It SHOULD NOT create a notification for permission changes to a group principal, even if

/// the user is in the group.

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ShareNotification<'a> {
    /// The id of the ShareNotification.

    pub id: Cow<'a, str>,
    /// The time this notification was created.

    pub created: UtcDate,
    /// Who made the change.

    pub changed_by: Person<'a>,
    /// The name of the data type for the object whose permissions have changed, e.g. “Calendar” or

    /// “Mailbox”.

    pub object_id: Cow<'a, str>,
    /// The id of the account where this object exists.

    pub object_account_id: Cow<'a, str>,
    /// The name of the object at the time the notification was made.

    pub name: Cow<'a, str>,
    /// The “myRights” property of the object for the user before the change.

    pub old_rights: Cow<'a, str>,
    /// The “myRights” property of the object for the user after the change.

    pub new_rights: Cow<'a, str>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Person<'a> {
    /// The name of the person who made the change.

    pub name: Cow<'a, str>,
    /// The email of the person who made the change, or null if no email is available.

    pub email: Option<Cow<'a, str>>,
    /// The id of the Principal corresponding to the person who made the change, or null if no

    /// associated principal.

    pub principal: Option<Cow<'a, str>>,
}
diff --git a/jogre-server/src/context/oauth2.rs b/jogre-server/src/context/oauth2.rs
index 3e74426..f1a1c60 100644
--- a/jogre-server/src/context/oauth2.rs
+++ a/jogre-server/src/context/oauth2.rs
@@ -325,7 +325,6 @@
        return AuthState::Unauthenticated(Some(UnauthenticatedState::InvalidCsrfToken));
    }

    // TODO: actually await here
    let Some(user) = store.get_by_username(username).await.unwrap() else {
        return AuthState::Unauthenticated(Some(UnauthenticatedState::InvalidUserPass));
    };
diff --git a/jogre-server/src/extensions/contacts.rs b/jogre-server/src/extensions/contacts.rs
new file mode 100644
index 0000000..8b5ecca 100644
--- /dev/null
+++ a/jogre-server/src/extensions/contacts.rs
@@ -1,0 +1,42 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::extensions::{JmapDataExtension, JmapExtension};

pub struct Contacts {}

impl JmapExtension for Contacts {
    const EXTENSION: &'static str = "urn:ietf:params:jmap:contacts";
}

impl JmapDataExtension<AddressBook> for Contacts {
    const ENDPOINT: &'static str = "AddressBook";
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ContactMetadata {
    pub may_create_address_book: bool,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AddressBook {
    id: Uuid,
    name: String,
    is_subscribed: bool,
    owner: Uuid,
    share_with: HashMap<Uuid, AddressBookRights>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::struct_excessive_bools)]
pub struct AddressBookRights {
    may_read: bool,
    may_write: bool,
    may_admin: bool,
    may_delete: bool,
}
diff --git a/jogre-server/src/extensions/core.rs b/jogre-server/src/extensions/core.rs
new file mode 100644
index 0000000..c0a709c 100644
--- /dev/null
+++ a/jogre-server/src/extensions/core.rs
@@ -1,0 +1,35 @@
use std::collections::BTreeSet;

use jmap_proto::endpoints::session::CoreCapability;
use uuid::Uuid;

use crate::{
    config::CoreCapabilities,
    extensions::{JmapExtension, JmapSessionCapabilityExtension},
};

#[derive(Clone)]
pub struct Core {
    pub(crate) core_capabilities: CoreCapabilities,
}

impl JmapExtension for Core {
    const EXTENSION: &'static str = "urn:ietf:params:jmap:core";
}

impl JmapSessionCapabilityExtension for Core {
    type Metadata = CoreCapability<'static>;

    fn build(&self, _user: Uuid) -> Self::Metadata {
        CoreCapability {
            max_size_upload: self.core_capabilities.max_size_upload.into(),
            max_concurrent_upload: self.core_capabilities.max_concurrent_upload.into(),
            max_size_request: self.core_capabilities.max_size_request.into(),
            max_concurrent_requests: self.core_capabilities.max_concurrent_requests.into(),
            max_calls_in_request: self.core_capabilities.max_calls_in_request.into(),
            max_objects_in_get: self.core_capabilities.max_objects_in_get.into(),
            max_objects_in_set: self.core_capabilities.max_objects_in_set.into(),
            collation_algorithms: BTreeSet::default(),
        }
    }
}
diff --git a/jogre-server/src/extensions/mod.rs b/jogre-server/src/extensions/mod.rs
new file mode 100644
index 0000000..28a4b17 100644
--- /dev/null
+++ a/jogre-server/src/extensions/mod.rs
@@ -1,0 +1,158 @@
use std::{borrow::Cow, collections::HashMap};

use jmap_proto::{extensions::sharing as proto_sharing, Value};
use serde::{
    de::{value::CowStrDeserializer, DeserializeSeed, MapAccess, Visitor},
    forward_to_deserialize_any, Deserialize, Deserializer, Serialize,
};
use uuid::Uuid;

pub mod contacts;
pub mod core;
pub mod sharing;

/// Defines a base extension to the JMAP specification.

pub trait JmapExtension {
    /// A URI that describes this extension (eg. `urn:ietf:params:jmap:contacts`).

    const EXTENSION: &'static str;
}

/// Defines an extension that can handle reads/writes.

pub trait JmapDataExtension<D>: JmapExtension {
    /// Endpoint from which this data type is exposed from (ie. `ContactBook`).

    const ENDPOINT: &'static str;
}

/// Defines an extension which should be exposed via session capabilities.

pub trait JmapSessionCapabilityExtension: JmapExtension {
    /// The metadata returned by this endpoint from the session endpoint.

    type Metadata: Serialize;

    fn build(&self, user: Uuid) -> Self::Metadata;
}

/// Defines an extension which should be exposed via account capabilities.

pub trait JmapAccountCapabilityExtension: JmapExtension {
    /// The metadata returned by this endpoint within account capabilities

    /// from the session endpoint.

    type Metadata: Serialize;

    fn build(&self, user: Uuid, account: Uuid) -> Self::Metadata;
}

/// Registry containing all extensions that can be handled by Jogre.

pub struct ExtensionRegistry {
    pub core: core::Core,
    pub contacts: contacts::Contacts,
    pub sharing_principals: sharing::Principals,
    pub sharing_principals_owner: sharing::PrincipalsOwner,
}

impl ExtensionRegistry {
    /// Builds the session capability payload from the .well-known/jmap endpoint

    pub fn build_session_capabilities(&self, user: Uuid) -> HashMap<Cow<'static, str>, Value> {
        let mut out = HashMap::new();
        out.insert(
            Cow::Borrowed(core::Core::EXTENSION),
            serde_json::to_value(JmapSessionCapabilityExtension::build(&self.core, user)).unwrap(),
        );
        out.insert(
            Cow::Borrowed(sharing::Principals::EXTENSION),
            serde_json::to_value(JmapSessionCapabilityExtension::build(
                &self.sharing_principals,
                user,
            ))
            .unwrap(),
        );
        out
    }
}

/// Defines all the data types that can be handled by our [`JmapDataExtension`]

/// extensions.

pub enum ConcreteData<'a> {
    AddressBook(contacts::AddressBook),
    Principal(proto_sharing::Principal<'a>),
    ShareNotification(proto_sharing::ShareNotification<'a>),
}

impl<'a> ConcreteData<'a> {
    /// Determines which extension should handle an incoming request by

    /// the defined endpoint, and deserializes the request into the

    /// relevant data type.

    pub fn parse(endpoint: &str, data: ResolvedArguments<'a>) -> Option<Self> {
        match endpoint {
            <contacts::Contacts as JmapDataExtension<contacts::AddressBook>>::ENDPOINT => {
                Some(Self::AddressBook(Deserialize::deserialize(data).unwrap()))
            }
            <sharing::Principals as JmapDataExtension<proto_sharing::Principal>>::ENDPOINT => {
                Some(Self::Principal(Deserialize::deserialize(data).unwrap()))
            },
            <sharing::Principals as JmapDataExtension<proto_sharing::ShareNotification>>::ENDPOINT => {
                Some(Self::ShareNotification(Deserialize::deserialize(data).unwrap()))
            },
            _ => None,
        }
    }
}

/// A list of key => value pairs representing the built parameters for the

/// incoming request with all references to other requests resolved.

pub struct ResolvedArguments<'a>(pub HashMap<Cow<'a, str>, Cow<'a, Value>>);

impl<'de> Deserializer<'de> for ResolvedArguments<'de> {
    type Error = serde_json::Error;

    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
    where
        V: Visitor<'de>,
    {
        visitor.visit_map(ResolvedArgumentsVisitor {
            iter: self.0.into_iter(),
            value: None,
        })
    }

    forward_to_deserialize_any! {
        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
        bytes byte_buf option unit unit_struct newtype_struct seq tuple
        tuple_struct map struct enum identifier ignored_any
    }
}

struct ResolvedArgumentsVisitor<'de> {
    iter: <HashMap<Cow<'de, str>, Cow<'de, Value>> as IntoIterator>::IntoIter,
    value: Option<Cow<'de, Value>>,
}

impl<'de> MapAccess<'de> for ResolvedArgumentsVisitor<'de> {
    type Error = serde_json::Error;

    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
    where
        K: DeserializeSeed<'de>,
    {
        let Some((key, value)) = self.iter.next() else {
            return Ok(None);
        };

        self.value = Some(value);

        seed.deserialize(CowStrDeserializer::new(key)).map(Some)
    }

    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
    where
        V: DeserializeSeed<'de>,
    {
        let value = self
            .value
            .take()
            .ok_or(serde::de::Error::custom("value is missing"))?;

        match value {
            Cow::Owned(v) => seed.deserialize(v),
            Cow::Borrowed(v) => seed.deserialize(v),
        }
    }
}
diff --git a/jogre-server/src/extensions/sharing.rs b/jogre-server/src/extensions/sharing.rs
new file mode 100644
index 0000000..b206864 100644
--- /dev/null
+++ a/jogre-server/src/extensions/sharing.rs
@@ -1,0 +1,67 @@
use jmap_proto::{
    common::Id,
    extensions::sharing::{
        Principal, PrincipalsAccountCapabilities, PrincipalsOwnerAccountCapabilities,
        PrincipalsSessionCapabilities, ShareNotification,
    },
};
use uuid::Uuid;

use crate::extensions::{
    JmapAccountCapabilityExtension, JmapDataExtension, JmapExtension,
    JmapSessionCapabilityExtension,
};

/// Represents support for the `Principal` and `ShareNotification` data types and associated API

/// methods.

pub struct Principals {}

impl JmapExtension for Principals {
    const EXTENSION: &'static str = "urn:ietf:params:jmap:principals";
}

impl JmapSessionCapabilityExtension for Principals {
    type Metadata = PrincipalsSessionCapabilities;

    fn build(&self, _user: Uuid) -> Self::Metadata {
        PrincipalsSessionCapabilities {}
    }
}

impl JmapAccountCapabilityExtension for Principals {
    type Metadata = PrincipalsAccountCapabilities<'static>;

    fn build(&self, _user: Uuid, _account: Uuid) -> Self::Metadata {
        PrincipalsAccountCapabilities {
            current_user_principal_id: None,
        }
    }
}

impl JmapDataExtension<Principal<'static>> for Principals {
    const ENDPOINT: &'static str = "Principal";
}

impl JmapDataExtension<ShareNotification<'static>> for Principals {
    const ENDPOINT: &'static str = "ShareNotification";
}

/// This URI is solely used as a key in an account’s accountCapabilities property;

/// it does not appear in the JMAP Session capabilities. Support is implied by the

/// `urn:ietf:params:jmap:principals` session capability.

pub struct PrincipalsOwner {}

impl JmapExtension for PrincipalsOwner {
    const EXTENSION: &'static str = "urn:ietf:params:jmap:principals:owner";
}

impl JmapAccountCapabilityExtension for PrincipalsOwner {
    type Metadata = PrincipalsOwnerAccountCapabilities<'static>;

    fn build(&self, _user: Uuid, _account: Uuid) -> Self::Metadata {
        PrincipalsOwnerAccountCapabilities {
            account_id_for_principal: Id("test".into()),
            principal_id: Id("test".into()),
        }
    }
}
diff --git a/jogre-server/src/methods/mod.rs b/jogre-server/src/methods/mod.rs
index 3d1ee69..8910624 100644
--- a/jogre-server/src/methods/mod.rs
+++ a/jogre-server/src/methods/mod.rs
@@ -1,9 +1,13 @@
mod api;
mod oauth;
mod session;

use std::sync::Arc;

use axum::{routing::get, Router};
use axum::{
    routing::{any, get},
    Router,
};
use tower::layer::layer_fn;
use tower_cookies::CookieManagerLayer;

@@ -15,6 +19,7 @@
pub fn router(context: Arc<Context>) -> Router {
    Router::new()
        .route("/.well-known/jmap", get(session::get))
        .route("/api/*", any(api::handle))
        // only apply auth requirement on endpoints above
        .layer(axum::middleware::from_fn_with_state(
            context.clone(),
diff --git a/jogre-server/src/methods/session.rs b/jogre-server/src/methods/session.rs
index ede525f..e1ee4b3 100644
--- a/jogre-server/src/methods/session.rs
+++ a/jogre-server/src/methods/session.rs
@@ -1,14 +1,12 @@
use std::{
    collections::{BTreeSet, HashMap},
    collections::HashMap,
    sync::{Arc, OnceLock},
};

use axum::{extract::State, Extension, Json};
use jmap_proto::{
    common::{Id, SessionState},
    endpoints::session::{
        Account, AccountCapabilities, CoreCapability, ServerCapabilities, Session,
    },
    endpoints::session::{Account, AccountCapabilities, Session},
};
use oxide_auth::primitives::grant::Grant;

@@ -66,18 +64,9 @@
    );

    Json(Session {
        capabilities: ServerCapabilities {
            core: CoreCapability {
                max_size_upload: context.core_capabilities.max_size_upload.into(),
                max_concurrent_upload: context.core_capabilities.max_concurrent_upload.into(),
                max_size_request: context.core_capabilities.max_size_request.into(),
                max_concurrent_requests: context.core_capabilities.max_concurrent_requests.into(),
                max_calls_in_request: context.core_capabilities.max_calls_in_request.into(),
                max_objects_in_get: context.core_capabilities.max_objects_in_get.into(),
                max_objects_in_set: context.core_capabilities.max_objects_in_set.into(),
                collation_algorithms: BTreeSet::default(),
            },
        },
        capabilities: context
            .extension_registry
            .build_session_capabilities(user.id),
        accounts,
        primary_accounts: HashMap::default(),
        username: username.into(),
diff --git a/jmap-proto/src/extensions/contacts/js_contact.rs b/jmap-proto/src/extensions/contacts/js_contact.rs
new file mode 100644
index 0000000..56646be 100644
--- /dev/null
+++ a/jmap-proto/src/extensions/contacts/js_contact.rs
@@ -1,0 +1,640 @@
use std::{borrow::Cow, collections::HashMap};

use chrono::NaiveDate;
use serde::{
    ser::SerializeMap, Deserialize, Serialize, Serializer, __private::ser::FlatMapSerializer,
};
use serde_json::Value;

use crate::common::{Id, UnsignedInt, UtcDate};

#[derive(Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct TypeWrapper<T>(T);

impl<T: Serialize + TypedStruct> Serialize for TypeWrapper<T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("@type", T::KIND)?;
        self.0.serialize(FlatMapSerializer(&mut map))?;
        map.end()
    }
}

pub trait TypedStruct {
    const KIND: &'static str;
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase", tag = "@type")]
pub enum Data<'a> {
    Card(#[serde(borrow)] Card<'a>),
    CardGroup(CardGroup<'a>),
}

/// A CardGroup object represents a group of cards. Its members may be Cards or CardGroups.

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CardGroup<'a> {
    /// An identifier, used to associate the object as the same across different

    /// systems, addressbooks and views.

    #[serde(borrow)]
    uid: Id<'a>,
    /// The set is represented as an object, with each key being the uid of another Card or

    /// CardGroup. The value for each key in the object MUST be true.

    members: HashMap<Id<'a>, bool>,
    /// The user-visible name for the group, e.g. "Friends". This may be any UTF-8 string of at

    /// least 1 character in length and maximum 255 octets in size. The same name may be used by

    /// two different groups.

    #[serde(default)]
    name: Cow<'a, str>,
    /// The card that represents this group.

    card: Option<Card<'a>>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Card<'a> {
    /// An identifier, used to associate the object as the same across different

    /// systems, addressbooks and views.

    #[serde(borrow)]
    uid: Id<'a>,
    /// The identifier for the product that created the Card object.

    prod_id: Option<Cow<'a, str>>,
    /// The date and time when this Card object was created.

    created: Option<UtcDate>,
    /// The date and time when the data in this Card object was last modified.

    updated: Option<UtcDate>,
    /// The kind of the entity the Card represents.

    kind: Option<CardKind>,
    /// Relates the object to other Card and CardGroup objects. This is

    /// represented as a map, where each key is the

    #[serde(default)]
    related_to: HashMap<Id<'a>, TypeWrapper<Relation>>,
    /// Language used for free-form text on this card.

    language: Option<Cow<'a, str>>,
    /// The name components of the name of the entity represented by this Card.

    #[serde(default)]
    name: Vec<TypeWrapper<NameComponent<'a>>>,
    /// The full name (e.g. the personal name and surname of an individual, the

    /// name of an organization) of the entity represented by this card. The

    /// purpose of this property is to define a name, even if the individual

    /// name components are not known. In addition, it is meant to provide

    /// alternative versions of the name for internationalisation.

    ///

    /// Implementations SHOULD prefer using the name property over this one

    /// and SHOULD NOT store the concatenated name component values in this

    /// property.

    #[serde(default)]
    full_name: Cow<'a, str>,
    /// The nick names of the entity represented by this card.

    #[serde(default)]
    nick_names: Vec<Cow<'a, str>>,
    /// The companies or organization names and units associated with this

    /// card.

    #[serde(default)]
    organizations: HashMap<Id<'a>, TypeWrapper<Organization<'a>>>,
    /// The job titles or functional positions of the entity represented by

    /// this card.

    #[serde(default)]
    titles: HashMap<Id<'a>, TypeWrapper<Title<'a>>>,
    /// The email addresses to contact the entity represented by this card.

    #[serde(default)]
    emails: HashMap<Id<'a>, TypeWrapper<EmailAddress<'a>>>,
    /// The phone numbers to contact the entity represented by this card.

    #[serde(default)]
    phones: HashMap<Id<'a>, TypeWrapper<Phone<'a>>>,
    /// The online resources and services that are associated with the entity

    /// represented by this card.

    #[serde(default)]
    online: HashMap<Id<'a>, TypeWrapper<Resource<'a>>>,
    /// A map of photo ids to File objects that contain photographs or images

    /// associated with this card. A typical use case is to include an avatar for display along the

    /// contact name.

    #[serde(default)]
    photos: HashMap<Id<'a>, TypeWrapper<File<'a>>>,
    /// Defines the preferred method to contact the holder of this card.

    preferred_contact_method: Option<PreferredContactMethod>,
    /// Defines the preferred languages for contacting the entity associated with this card. The

    /// keys in the object MUST be [RFC5646] language tags. The values are a (possibly empty) list

    /// of contact language preferences for this language. A valid ContactLanguage object MUST have

    /// at least one of its properties set.

    #[serde(default)]
    preferred_contact_languages: HashMap<String, TypeWrapper<ContactLanguage>>,
    /// A map of address ids to Address objects, containing physical locations.

    #[serde(default)]
    address: HashMap<Id<'a>, TypeWrapper<Address<'a>>>,
    /// A map of language tags [RFC5646] to patches, which localize a property value into the

    /// locale of the respective language tag.

    ///

    /// A patch MUST NOT target the localizations property.

    #[serde(default)]
    localizations: HashMap<Cow<'a, str>, Value>,
    /// These are memorable dates and events for the entity represented by this card.

    #[serde(default)]
    anniversaries: HashMap<Id<'a>, TypeWrapper<Anniversary<'a>>>,
    /// Defines personal information about the entity represented by this card.

    #[serde(default)]
    personal_info: HashMap<Id<'a>, TypeWrapper<PersonalInfo<'a>>>,
    /// Arbitrary notes about the entity represented by this card.

    #[serde(default)]
    notes: Cow<'a, str>,
    /// The set of free-text or URI categories that relate to the card. The set is represented as

    /// an object, with each key being a category. The value for each key in the object MUST be

    /// true.

    #[serde(default)]
    categories: HashMap<Cow<'a, str>, bool>,
    ///  Maps identifiers of custom time zones to their time zone definitions. For a description of

    /// this property see the timeZones property definition in [RFC8984].

    #[serde(default)]
    time_zones: HashMap<Cow<'a, str>, Value>,
}

/// Defines personal information about the entity represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PersonalInfo<'a> {
    /// Specifies the type for this personal information.

    #[serde(rename = "type")]
    type_: PersonalInfoType,
    /// The actual information. This generally is free-text, but future

    /// specifications MAY restrict allowed values depending on the type of

    /// this PersonalInformation.

    value: Cow<'a, str>,
    /// Indicates the level of expertise, or engagement in hobby or interest.

    level: Option<PersonalInfoLevel>,
}

impl TypedStruct for PersonalInfo<'_> {
    const KIND: &'static str = "PersonalInfo";
}

/// Specifies the type for this personal information.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PersonalInfoType {
    /// A field of expertise or credential

    Expertise,
    /// A hobby

    Hobby,
    /// An interest

    Interest,
    /// An information not covered by the above categories

    Other,
}

/// Indicates the level of expertise, or engagement in hobby or interest.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PersonalInfoLevel {
    High,
    Medium,
    Low,
}

/// These are memorable dates and events for the entity represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Anniversary<'a> {
    /// Specifies the type of the anniversary.

    #[serde(rename = "type")]
    type_: AnniversaryType,
    /// A label describing the value in more detail, especially if the type

    /// property has value other (but MAY be included with any type).

    #[serde(default)]
    label: Cow<'a, str>,
    /// The date of this anniversary, in the form "YYYY-MM-DD"

    /// (any part may be all 0s for unknown) or a [RFC3339] timestamp.

    date: NaiveDate,
    /// An address associated with this anniversary, e.g. the place of birth or

    /// death.

    place: Option<Address<'a>>,
}

impl TypedStruct for Anniversary<'_> {
    const KIND: &'static str = "Anniversary";
}

/// Specifies the type of the anniversary.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum AnniversaryType {
    /// A birth day anniversary

    Birth,
    /// A death day anniversary

    Death,
    /// An anniversary not covered by any of the known types.

    Other,
}

/// A physical location.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Address<'a> {
    ///  The complete address, excluding type and label. This property is mainly useful to

    /// represent addresses of which the individual address components are unknown, or to provide

    /// localized representations.

    #[serde(default)]
    full_address: Cow<'a, str>,
    ///  The street address. The concatenation of the component values, separated by whitespace,

    /// SHOULD result in a valid street address for the address locale. Doing so, implementations

    /// MAY ignore any separator components. The StreetComponent object type is defined in the

    /// paragraph below.

    #[serde(default)]
    street: Vec<TypeWrapper<StreetComponent<'a>>>,
    /// The city, town, village, post town, or other locality within which the street address may

    /// be found.

    #[serde(default)]
    locality: Cow<'a, str>,
    /// The province, such as a state, county, or canton within which the locality may be found.

    #[serde(default)]
    region: Cow<'a, str>,
    /// The country name.

    #[serde(default)]
    country: Cow<'a, str>,
    /// The postal code, post code, ZIP code or other short code associated with the address by the

    /// relevant country's postal system.

    #[serde(default)]
    postcode: Cow<'a, str>,
    /// The ISO-3166-1 country code.

    #[serde(default)]
    country_code: Cow<'a, str>,
    /// A [RFC5870] "geo:" URI for the address.

    #[serde(default)]
    coordinates: Cow<'a, str>,
    /// Identifies the time zone this address is located in. This either MUST be a time zone name

    /// registered in the IANA Time Zone Database, or it MUST be a valid TimeZoneId as defined in

    /// [RFC8984]. For the latter, a corresponding time zone MUST be defined in the timeZones

    /// property.

    #[serde(default)]
    time_zone: Cow<'a, str>,
    /// The contexts of the address information.

    #[serde(default)]
    context: HashMap<AddressContext, bool>,
    /// A label describing the value in more detail.

    #[serde(default)]
    label: Cow<'a, str>,
    ///  The preference of this address in relation to other addresses.

    pref: Option<Preference>,
}

impl TypedStruct for Address<'_> {
    const KIND: &'static str = "Address";
}

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase", untagged)]
pub enum AddressContext {
    /// An address to be used for billing.

    Billing,
    /// An address to be used for delivering physical items

    Postal,
    /// A normal context

    Other(Context),
}

///  The street address. The concatenation of the component values, separated by whitespace, SHOULD

/// result in a valid street address for the address locale. Doing so, implementations MAY ignore

/// any separator components. The StreetComponent object type is defined in the paragraph below.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct StreetComponent<'a> {
    /// The type of this street component.

    #[serde(rename = "type")]
    type_: StreetComponentKind,
    /// The value of this street component.

    value: Cow<'a, str>,
}

impl TypedStruct for StreetComponent<'_> {
    const KIND: &'static str = "StreetComponent";
}

/// The type of this street component.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum StreetComponentKind {
    Name,
    Number,
    Apartment,
    Room,
    Extension,
    Direction,
    Building,
    Floor,
    PostOfficeBox,
    Separator,
    Unknown,
}

/// Defines the preferred method to contact the holder of this card.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ContactLanguage {
    /// Defines the context in which to use this language.

    context: Option<Context>,
    /// Defines the preference of this language in relation to other

    /// languages of the same context.

    pref: Option<Preference>,
}

impl TypedStruct for ContactLanguage {
    const KIND: &'static str = "ContactLanguage";
}

/// Defines the preferred method to contact the holder of this card.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PreferredContactMethod {
    Emails,
    Phones,
    Online,
}

/// A map of photo ids to File objects that contain photographs or images

/// associated with this card. A typical use case is to include an avatar for display along the

/// contact name.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct File<'a> {
    /// A URI where to fetch the data of this file.

    href: Cow<'a, str>,
    /// The content-type of the file, if known.

    media_type: Cow<'a, str>,
    /// The size, in octets, of the file when fully decoded (i.e., the number

    /// of octets in the file the user would download), if known.

    size: Option<UnsignedInt>,
    /// The preference of this photo in relation to other photos.

    pref: Option<Preference>,
}

impl TypedStruct for File<'_> {
    const KIND: &'static str = "File";
}

/// The online resources and services that are associated with the entity

/// represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Resource<'a> {
    /// resource value, where the allowed value form is defined by the the type

    /// property. In any case the value MUST NOT be empty.

    resource: Cow<'a, str>,
    /// The type of the resource value.

    #[serde(rename = "type")]
    type_: ResourceType,
    /// Used for URI resource values. Provides the media type [RFC2046] of the

    /// resource identified by the URI.

    #[serde(default)]
    media_type: Cow<'a, str>,
    /// The contexts in which to use this resource.

    #[serde(default)]
    context: HashMap<Context, bool>,
    /// A label describing the value in more detail, especially if the type

    /// property has value other (but MAY be included with any type).

    #[serde(default)]
    label: Cow<'a, str>,
    /// The preference of this resource in relation to other resources.

    pref: Option<Preference>,
}

impl TypedStruct for Resource<'_> {
    const KIND: &'static str = "Resource";
}

/// The type of the resource value.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ResourceType {
    /// The resource value is a URI, e.g. a website link. This MUST be a valid URI as defined in

    /// Section 3 of [RFC3986] and updates.

    Uri,
    /// The resource value is a username associated with the entity represented by this card (e.g.

    /// for social media, or an IM client). The label property SHOULD be included to identify what

    /// service this is for. For compatibility between clients, this label SHOULD be the canonical

    /// service name, including capitalisation. e.g. Twitter, Facebook, Skype, GitHub, XMPP. The

    /// resource value may be any non-empty free text.

    Username,
    /// The resource value is something else not covered by the above categories. A label property

    /// MAY be included to display next to the number to help the user identify its purpose. The

    /// resource value may be any non-empty free text.

    Other,
}

/// The phone numbers to contact the entity represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Phone<'a> {
    /// The phone value, as either a URI or a free-text phone number. Typical

    /// URI schemes are the [RFC3966] tel or [RFC3261] sip schemes, but any

    /// URI scheme is allowed.

    phone: Cow<'a, str>,
    ///  The set of contact features that this phone number may be used for. The

    /// set is represented as an object, with each key being a method type. The

    /// value for each key in the object MUST be true.

    #[serde(default)]
    features: HashMap<PhoneFeature, bool>,
    // The contexts in which to use this number. The value for each
    /// key in the object MUST be true.

    #[serde(default)]
    contexts: HashMap<Context, bool>,
    /// A label describing the value in more detail, especially if the type

    /// property has value other (but MAY be included with any type).

    #[serde(default)]
    label: Cow<'a, str>,
    /// The preference of this email address in relation to other email addresses.

    pref: Option<Preference>,
}

impl TypedStruct for Phone<'_> {
    const KIND: &'static str = "Phone";
}

/// The email addresses to contact the entity represented by this card.

#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum PhoneFeature {
    /// The number is for calling by voice.

    Voice,
    /// The number is for sending faxes.

    Fax,
    /// The number is for a pager or beeper.

    Pager,
    /// The number supports text messages (SMS).

    Text,
    /// The number is for a cell phone.

    Cell,
    /// The number is for a device for people with hearing or speech difficulties.

    Textphone,
    /// The number supports video conferencing.

    Video,
    /// The number is for some other purpose. The label property MAY be included

    /// to display next to the number to help the user identify its purpose.

    Other,
}

/// The email addresses to contact the entity represented by this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct EmailAddress<'a> {
    /// The email address. This MUST be an addr-spec value as defined in

    /// Section 3.4.1 of [RFC5322].

    email: Cow<'a, str>,
    /// The contexts in which to use this email address. The value for each

    /// key in the object MUST be true.

    #[serde(default)]
    contexts: HashMap<Context, bool>,
    /// The preference of this email address in relation to other email addresses.

    pref: Option<Preference>,
}

impl TypedStruct for EmailAddress<'_> {
    const KIND: &'static str = "EmailAddress";
}

/// This data type allows to define a preference order on same-typed contact

/// information. For example, a card holder may have two email addresses and

/// prefer to be contacted with one of them.

///

/// A preference value MUST be an integer number in the range 1 and 100. Lower

/// values correspond to a higher level of preference, with 1 being most

/// preferred. If no preference is set, then the contact information MUST be

/// interpreted as being least preferred.

///

/// Note that the preference only is defined in relation to contact information

/// of the same type. For example, the preference orders within emails and

/// phone numbers are indendepent of each other. Also note that the

/// preferredContactMethod property allows to define a preferred contact method

/// across method types.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Preference(u8);

/// The companies or organization names and units associated with this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Title<'a> {
    /// The name of this organization.

    #[serde(borrow)]
    name: Cow<'a, str>,
    /// The id of the organization in which this title is held.

    #[serde(default)]
    organization: Vec<Id<'a>>,
}

impl TypedStruct for Title<'_> {
    const KIND: &'static str = "Title";
}

/// The companies or organization names and units associated with this card.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Organization<'a> {
    ///  The name of this organization.

    name: Cow<'a, str>,
    ///  Additional levels of organizational unit names.

    #[serde(default)]
    units: Vec<Cow<'a, str>>,
}

impl TypedStruct for Organization<'_> {
    const KIND: &'static str = "Organization";
}

/// The name components of the name of the entity represented by this Card. Name

/// components SHOULD be ordered such that their values joined by whitespace

/// produce a valid full name of this entity. Doing so, implementations MAY

/// ignore any separator components.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct NameComponent<'a> {
    value: Cow<'a, str>,
    #[serde(rename = "type")]
    type_: NameComponentKind,
}

impl TypedStruct for NameComponent<'_> {
    const KIND: &'static str = "NameComponent";
}

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum NameComponentKind {
    /// The value is a honorific title(s), e.g. "Mr", "Ms", "Dr".

    Prefix,
    /// The value is a personal name(s), also known as "first name", "given name".

    Personal,
    /// The value is a surname, also known as "last name", "family name".

    Surname,
    /// The value is an additional name, also known as "middle name".

    Additional,
    /// The value is a honorific suffix, e.g. "B.A.", "Esq.".

    Suffix,
    /// A separator for two name components. The value property of the component

    /// includes the verbatim separator, for example a newline character.

    Separator,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Relation {
    relation: HashMap<RelationKind, bool>,
}

impl TypedStruct for Relation {
    const KIND: &'static str = "Relation";
}

/// Contact information typically is associated with a context in which it

/// should be used. For example, someone might have distinct phone numbers

/// for work and private contexts. The Context data type enumerates common

/// contexts.

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Context {
    /// The contact information may be used to contact the card holder in a

    /// private context.

    Private,
    /// The contact information may be used to contact the card holder in a

    /// professional context.

    Work,
    /// The contact information may be used to contact the card holder in some

    /// other context.

    Other,
}

#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum RelationKind {
    Contact,
    Acquaintance,
    Friend,
    Met,
    CoWorker,
    Colleague,
    CoResident,
    Neighbor,
    Child,
    Parent,
    Sibling,
    Spouse,
    Kin,
    Muse,
    Crush,
    Date,
    Sweetheart,
    Me,
    Agent,
    Emergency,
}

/// The kind of the entity the Card represents.

#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum CardKind {
    /// A single person

    Individual,
    /// An organization

    Org,
    /// A named location

    Location,
    /// A device, such as appliances, computers, or network elements

    Device,
    /// A software application

    Application,
}
diff --git a/jmap-proto/src/extensions/contacts/mod.rs b/jmap-proto/src/extensions/contacts/mod.rs
new file mode 100644
index 0000000..034ca9c 100644
--- /dev/null
+++ a/jmap-proto/src/extensions/contacts/mod.rs
@@ -1,0 +1,1 @@
pub mod js_contact;
diff --git a/jogre-server/src/methods/api/mod.rs b/jogre-server/src/methods/api/mod.rs
new file mode 100644
index 0000000..ba2c847 100644
--- /dev/null
+++ a/jogre-server/src/methods/api/mod.rs
@@ -1,0 +1,100 @@
use std::{borrow::Cow, collections::HashMap, sync::Arc};

use axum::{body::Bytes, extract::State, Extension};
use jmap_proto::{
    common::SessionState,
    endpoints::{Argument, Arguments, Invocation, Request, Response},
    errors::MethodError,
};
use oxide_auth::primitives::grant::Grant;

use crate::{
    context::Context,
    extensions::{ConcreteData, ResolvedArguments},
    store::UserProvider,
};

pub async fn handle(
    State(context): State<Arc<Context>>,
    Extension(grant): Extension<Grant>,
    body: Bytes,
) {
    let payload: Request<'_> = serde_json::from_slice(&body).unwrap();

    // TODO: `using`
    // TODO: `method_calls`
    // TODO: `created_ids`

    let username = grant.owner_id;

    let user = context
        .store
        .get_by_username(&username)
        .await
        .unwrap()
        .unwrap();

    let session_state = context
        .store
        .fetch_seq_number_for_user(user.id)
        .await
        .unwrap();

    let mut response = Response {
        method_responses: Vec::with_capacity(payload.method_calls.len()),
        created_ids: None,
        session_state: SessionState(session_state.to_string().into()),
    };

    for invocation_request in payload.method_calls {
        let Some(resolved_arguments) = resolve_arguments(&response, invocation_request.arguments)
        else {
            response.method_responses.push(
                MethodError::InvalidResultReference.into_invocation(invocation_request.request_id),
            );
            continue;
        };

        let Some(_request) =
            ConcreteData::parse(invocation_request.name.as_ref(), resolved_arguments)
        else {
            response
                .method_responses
                .push(MethodError::UnknownMethod.into_invocation(invocation_request.request_id));
            continue;
        };

        // TODO: call handler

        response.method_responses.push(Invocation {
            name: invocation_request.name,
            arguments: Arguments(HashMap::new()),
            request_id: invocation_request.request_id,
        });
    }
}

fn resolve_arguments<'a>(
    response: &'a Response,
    args: Arguments<'a>,
) -> Option<ResolvedArguments<'a>> {
    let mut res = HashMap::with_capacity(args.0.len());

    for (key, value) in args.0 {
        let value = match value {
            Argument::Reference(refer) => {
                let referenced_response = response
                    .method_responses
                    .iter()
                    .find(|inv| inv.request_id == refer.result_of && inv.name == refer.name)?;

                referenced_response.arguments.pointer(&refer.path)?
            }
            Argument::Absolute(value) => Cow::Owned(value),
        };

        res.insert(key, value);
    }

    Some(ResolvedArguments(res))
}