Undelete: deleted accounts are kept for their period, hold their addresses, and are restored or destroyed through inbuxa:DeletedAccount (UD-15 to UD-17a)
With archiveDeletedAccountsFor set, a destroyed account's record is kept in the fork subspace with its id, its DestroyAccount task is due at the end of the period, and its shares are suspended both ways. Its addresses can't be taken by new accounts, aliases, lists or masks. inbuxa:DeletedAccount/get lists kept accounts to server and tenant administrators; /set restores one with a new password (same id, task cancelled, shares reinstated) or destroys it now. The destroy task also clears undelete's own records. Acceptance test 14; test 16 written as the ignored undelete_compat.
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
//! `inbuxa:DeletedAccount/get` and `/set` under `urn:inbuxa:jmap`: deleted
|
||||
//! accounts kept for their period, listed, restored or destroyed for good
|
||||
//! (undelete spec, UD-15 to UD-17).
|
||||
|
||||
use crate::{
|
||||
object::{AnyId, JmapObject, JmapObjectId},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Element, Key, Property};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DeletedAccount;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DeletedAccountProperty {
|
||||
Id,
|
||||
Name,
|
||||
Addresses,
|
||||
MemberTenantId,
|
||||
DeletedAt,
|
||||
KeptUntil,
|
||||
Restore,
|
||||
Password,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DeletedAccountValue {
|
||||
Id(Id),
|
||||
Date(UTCDate),
|
||||
}
|
||||
|
||||
impl Property for DeletedAccountProperty {
|
||||
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
|
||||
DeletedAccountProperty::parse(value)
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
DeletedAccountProperty::Id => "id",
|
||||
DeletedAccountProperty::Name => "name",
|
||||
DeletedAccountProperty::Addresses => "addresses",
|
||||
DeletedAccountProperty::MemberTenantId => "memberTenantId",
|
||||
DeletedAccountProperty::DeletedAt => "deletedAt",
|
||||
DeletedAccountProperty::KeptUntil => "keptUntil",
|
||||
DeletedAccountProperty::Restore => "restore",
|
||||
DeletedAccountProperty::Password => "password",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DeletedAccountProperty {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
b"id" => DeletedAccountProperty::Id,
|
||||
b"name" => DeletedAccountProperty::Name,
|
||||
b"addresses" => DeletedAccountProperty::Addresses,
|
||||
b"memberTenantId" => DeletedAccountProperty::MemberTenantId,
|
||||
b"deletedAt" => DeletedAccountProperty::DeletedAt,
|
||||
b"keptUntil" => DeletedAccountProperty::KeptUntil,
|
||||
b"restore" => DeletedAccountProperty::Restore,
|
||||
b"password" => DeletedAccountProperty::Password,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DeletedAccountProperty {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
DeletedAccountProperty::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for DeletedAccountValue {
|
||||
type Property = DeletedAccountProperty;
|
||||
|
||||
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
|
||||
match key {
|
||||
Key::Property(DeletedAccountProperty::Id | DeletedAccountProperty::MemberTenantId) => {
|
||||
Id::from_str(value).ok().map(DeletedAccountValue::Id)
|
||||
}
|
||||
Key::Property(DeletedAccountProperty::DeletedAt | DeletedAccountProperty::KeptUntil) => {
|
||||
UTCDate::from_str(value).ok().map(DeletedAccountValue::Date)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cow(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
DeletedAccountValue::Id(id) => id.to_string().into(),
|
||||
DeletedAccountValue::Date(date) => date.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObject for DeletedAccount {
|
||||
type Property = DeletedAccountProperty;
|
||||
|
||||
type Element = DeletedAccountValue;
|
||||
|
||||
type Id = Id;
|
||||
|
||||
type Filter = ();
|
||||
|
||||
type Comparator = ();
|
||||
|
||||
type GetArguments = ();
|
||||
|
||||
type SetArguments<'de> = ();
|
||||
|
||||
type QueryArguments = ();
|
||||
|
||||
type CopyArguments = ();
|
||||
|
||||
type ParseArguments = ();
|
||||
|
||||
const ID_PROPERTY: Self::Property = DeletedAccountProperty::Id;
|
||||
}
|
||||
|
||||
impl From<Id> for DeletedAccountValue {
|
||||
fn from(id: Id) -> Self {
|
||||
DeletedAccountValue::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for DeletedAccountValue {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
match self {
|
||||
DeletedAccountValue::Id(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
match self {
|
||||
DeletedAccountValue::Id(id) => Some(AnyId::Id(*id)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, new_id: AnyId) -> bool {
|
||||
if let AnyId::Id(id) = new_id {
|
||||
*self = DeletedAccountValue::Id(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapObjectId for DeletedAccountProperty {
|
||||
fn as_id(&self) -> Option<Id> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_id(&self) -> Option<AnyId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_id_ref(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_set_id(&mut self, _: AnyId) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ pub mod contact;
|
||||
pub mod email;
|
||||
pub mod email_submission;
|
||||
pub mod fastmail_masked_email; // inbuxa: masked email
|
||||
pub mod inbuxa_deleted_account; // inbuxa: undelete
|
||||
pub mod file_node;
|
||||
pub mod identity;
|
||||
pub mod mailbox;
|
||||
|
||||
@@ -53,6 +53,9 @@ impl Response<'_> {
|
||||
GetResponseMethod::MaskedEmail(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::DeletedAccount(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
GetResponseMethod::Principal(response) => {
|
||||
response.eval_jptr(path, &mut results)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ impl Response<'_> {
|
||||
GetRequestMethod::Sieve(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::VacationResponse(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::MaskedEmail(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::DeletedAccount(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Principal(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Quota(request) => request.resolve_references(self)?,
|
||||
GetRequestMethod::Blob(request) => request.resolve_references(self)?,
|
||||
@@ -79,6 +80,9 @@ impl Response<'_> {
|
||||
SetRequestMethod::MaskedEmail(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::DeletedAccount(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
SetRequestMethod::AddressBook(request) => {
|
||||
request.resolve_references(self, 1, false)?
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ pub enum MethodObject {
|
||||
Registry(ObjectType),
|
||||
// inbuxa: Fastmail's MaskedEmail
|
||||
MaskedEmail,
|
||||
// inbuxa: deleted accounts (UD-17)
|
||||
DeletedAccount,
|
||||
}
|
||||
|
||||
impl MethodObject {
|
||||
@@ -67,6 +69,7 @@ impl MethodObject {
|
||||
MethodObject::FileNode => Capability::FileNode,
|
||||
MethodObject::Registry(_) => Capability::Stalwart,
|
||||
MethodObject::MaskedEmail => Capability::FastmailMaskedEmail,
|
||||
MethodObject::DeletedAccount => Capability::Inbuxa,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,6 +243,8 @@ impl MethodName {
|
||||
(MethodFunction::Echo, MethodObject::Core) => "Core/echo",
|
||||
(MethodFunction::Get, MethodObject::MaskedEmail) => "MaskedEmail/get",
|
||||
(MethodFunction::Set, MethodObject::MaskedEmail) => "MaskedEmail/set",
|
||||
(MethodFunction::Get, MethodObject::DeletedAccount) => "inbuxa:DeletedAccount/get",
|
||||
(MethodFunction::Set, MethodObject::DeletedAccount) => "inbuxa:DeletedAccount/set",
|
||||
(method, MethodObject::Registry(obj)) => {
|
||||
return Cow::Owned(format!("x:{}/{}", obj.as_str(), method.as_str()));
|
||||
}
|
||||
@@ -361,6 +366,8 @@ impl MethodName {
|
||||
|
||||
"MaskedEmail/get" => (MethodObject::MaskedEmail, MethodFunction::Get),
|
||||
"MaskedEmail/set" => (MethodObject::MaskedEmail, MethodFunction::Set),
|
||||
"inbuxa:DeletedAccount/get" => (MethodObject::DeletedAccount, MethodFunction::Get),
|
||||
"inbuxa:DeletedAccount/set" => (MethodObject::DeletedAccount, MethodFunction::Set),
|
||||
|
||||
).or_else(|| {
|
||||
let (obj, fnc) = s.strip_prefix("x:")?.split_once('/')?;
|
||||
@@ -412,6 +419,7 @@ impl Display for MethodObject {
|
||||
MethodObject::CalendarEventNotification => "CalendarEventNotification",
|
||||
MethodObject::ShareNotification => "ShareNotification",
|
||||
MethodObject::MaskedEmail => "MaskedEmail",
|
||||
MethodObject::DeletedAccount => "inbuxa:DeletedAccount",
|
||||
MethodObject::Registry(obj) => {
|
||||
f.write_str("x:")?;
|
||||
return f.write_str(obj.as_str());
|
||||
|
||||
@@ -112,6 +112,7 @@ pub enum GetRequestMethod {
|
||||
ShareNotification(Box<GetRequest<ShareNotification>>),
|
||||
Registry(Box<GetRequest<Registry>>),
|
||||
MaskedEmail(Box<GetRequest<crate::object::fastmail_masked_email::FastmailMaskedEmail>>),
|
||||
DeletedAccount(Box<GetRequest<crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -133,6 +134,7 @@ pub enum SetRequestMethod<'x> {
|
||||
ParticipantIdentity(Box<SetRequest<'x, ParticipantIdentity>>),
|
||||
Registry(Box<SetRequest<'x, Registry>>),
|
||||
MaskedEmail(Box<SetRequest<'x, crate::object::fastmail_masked_email::FastmailMaskedEmail>>),
|
||||
DeletedAccount(Box<SetRequest<'x, crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -153,6 +153,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::DeletedAccount) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::DeletedAccount(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Get, MethodObject::VacationResponse) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Get(GetRequestMethod::VacationResponse(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
@@ -304,6 +311,13 @@ impl<'de> Visitor<'de> for CallVisitor {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::DeletedAccount) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::DeletedAccount(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
Ok(None) => {
|
||||
return Err(de::Error::invalid_length(1, &self));
|
||||
}
|
||||
},
|
||||
(MethodFunction::Set, MethodObject::VacationResponse) => match seq.next_element() {
|
||||
Ok(Some(value)) => RequestMethod::Set(SetRequestMethod::VacationResponse(value)),
|
||||
Err(err) => RequestMethod::invalid(err),
|
||||
|
||||
@@ -99,6 +99,7 @@ pub enum GetResponseMethod {
|
||||
ShareNotification(GetResponse<ShareNotification>),
|
||||
Registry(GetResponse<Registry>),
|
||||
MaskedEmail(GetResponse<crate::object::fastmail_masked_email::FastmailMaskedEmail>),
|
||||
DeletedAccount(GetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
@@ -121,6 +122,7 @@ pub enum SetResponseMethod {
|
||||
ParticipantIdentity(Box<SetResponse<ParticipantIdentity>>),
|
||||
Registry(Box<SetResponse<Registry>>),
|
||||
MaskedEmail(Box<SetResponse<crate::object::fastmail_masked_email::FastmailMaskedEmail>>),
|
||||
DeletedAccount(Box<SetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
@@ -280,6 +282,19 @@ impl<'x> From<SetResponse<crate::object::fastmail_masked_email::FastmailMaskedEm
|
||||
}
|
||||
}
|
||||
|
||||
// inbuxa: deleted accounts (UD-17)
|
||||
impl<'x> From<GetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::DeletedAccount(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<SetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>> for ResponseMethod<'x> {
|
||||
fn from(value: SetResponse<crate::object::inbuxa_deleted_account::DeletedAccount>) -> Self {
|
||||
ResponseMethod::Set(SetResponseMethod::DeletedAccount(Box::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> From<GetResponse<VacationResponse>> for ResponseMethod<'x> {
|
||||
fn from(value: GetResponse<VacationResponse>) -> Self {
|
||||
ResponseMethod::Get(GetResponseMethod::VacationResponse(value))
|
||||
|
||||
Reference in New Issue
Block a user