Files
inbuxa-server/crates/jmap-proto/src/object/inbuxa_tenant_protocol_policy.rs
T
jcoffey-dev 8e9cedbe97
ci / fork-checks (pull_request) Successful in 43s
ci / build (pull_request) Successful in 7m40s
Give IMAP, POP3 and ManageSieve a switch each
The legacy-protocols switch was all or nothing. An operator can now stop
POP3 and keep IMAP: each of IMAP, POP3 and ManageSieve has its own
switch, server-wide on inbuxa:ProtocolPolicy and per tenant on
inbuxa:TenantProtocolPolicy (properties imap, pop3, manageSieve).

legacyProtocols stays as the kill-all: setting it sets all three, and it
reads "disabled" exactly when all three are off. A policy stored before
this has only legacyProtocols and reads as all three at that value, so
existing servers and tenants carry over unchanged. In one /set, a
protocol named beside legacyProtocols overrides it.

SMTP submission keeps no switch of its own: sign-in over it is refused
only when all three are off, as the single switch did (LP-6), so
turning one protocol off never stops a mail app sending. For a tenant,
the server's switches and the tenant's count together.

Server-wide, a change closes the listeners of whatever is now off and
puts back the saved listeners of whatever is on again, both in one
change if asked; listeners of a protocol still off stay saved. Sign-in,
autoconfig, autodiscover, PACC (now prepared once per combination) and
the suggested DNS records all follow each protocol separately. A tenant
may turn a protocol on only while the server has it on (LP-9), and the
refusal names which. The JMAP session adds legacyAllowed, the protocols
still allowed for the account; legacyProtocols there keeps its meaning
for older webmail builds. Events name the switches ("pop3 disabled"),
and audit before/after reads every switch even from an older policy.

Tested: unit tests for the switches, the old-policy reading, the
server/tenant combination, the tenant refusal and listener refusal; and
tests/e2e/legacy_protocols.py against a running server, all 100 checks,
including new ones: POP3 alone off closes only its port and refuses
only its sign-in while IMAP and sending go on; only POP3 stops being
advertised; one change closes IMAP and reopens POP3; a tenant turns
POP3 off for itself, and can't turn IMAP on while the server has it off.
2026-09-27 23:12:32 -07:00

198 lines
5.6 KiB
Rust

/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! `inbuxa:TenantProtocolPolicy/get` and `/set` under `urn:inbuxa:jmap`: one
//! tenant's legacy mail protocols switch (legacy-protocols spec, LP-9 to
//! LP-14). One per tenant; its id is the tenant's id.
//!
//! `tenantId`, `changedAt` and `changedBy` are the server's to say. A client
//! that sets them is answered with `invalidProperties`.
use crate::object::{AnyId, JmapObject, JmapObjectId};
use jmap_tools::{Element, Key, Property};
use std::{borrow::Cow, str::FromStr};
use types::id::Id;
#[derive(Debug, Clone, Default)]
pub struct TenantProtocolPolicy;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TenantProtocolPolicyProperty {
Id,
/// Server-set: the tenant this is the switch of.
TenantId,
/// The kill-all: `enabled` or `disabled`; reads `disabled` when all
/// three protocols are off, and sets all three.
LegacyProtocols,
/// Each protocol's own switch: `enabled` or `disabled`.
Imap,
Pop3,
ManageSieve,
ChangedAt,
ChangedBy,
/// Server-set: who signed in over a legacy protocol in the last 30
/// days, and when, for the impact panel (LP-15).
RecentLegacyUse,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TenantProtocolPolicyValue {
Id(Id),
}
impl Property for TenantProtocolPolicyProperty {
fn try_parse(_: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
TenantProtocolPolicyProperty::parse(value)
}
fn to_cow(&self) -> Cow<'static, str> {
match self {
TenantProtocolPolicyProperty::Id => "id",
TenantProtocolPolicyProperty::TenantId => "tenantId",
TenantProtocolPolicyProperty::LegacyProtocols => "legacyProtocols",
TenantProtocolPolicyProperty::Imap => "imap",
TenantProtocolPolicyProperty::Pop3 => "pop3",
TenantProtocolPolicyProperty::ManageSieve => "manageSieve",
TenantProtocolPolicyProperty::ChangedAt => "changedAt",
TenantProtocolPolicyProperty::ChangedBy => "changedBy",
TenantProtocolPolicyProperty::RecentLegacyUse => "recentLegacyUse",
}
.into()
}
}
impl TenantProtocolPolicyProperty {
fn parse(value: &str) -> Option<Self> {
hashify::tiny_map!(value.as_bytes(),
b"id" => TenantProtocolPolicyProperty::Id,
b"tenantId" => TenantProtocolPolicyProperty::TenantId,
b"legacyProtocols" => TenantProtocolPolicyProperty::LegacyProtocols,
b"imap" => TenantProtocolPolicyProperty::Imap,
b"pop3" => TenantProtocolPolicyProperty::Pop3,
b"manageSieve" => TenantProtocolPolicyProperty::ManageSieve,
b"changedAt" => TenantProtocolPolicyProperty::ChangedAt,
b"changedBy" => TenantProtocolPolicyProperty::ChangedBy,
b"recentLegacyUse" => TenantProtocolPolicyProperty::RecentLegacyUse,
)
}
}
impl TenantProtocolPolicyProperty {
/// Whether this property is the server's to say. A client that sets one
/// is answered with `invalidProperties`.
pub fn is_server_set(&self) -> bool {
matches!(
self,
TenantProtocolPolicyProperty::TenantId
| TenantProtocolPolicyProperty::ChangedAt
| TenantProtocolPolicyProperty::ChangedBy
| TenantProtocolPolicyProperty::RecentLegacyUse
)
}
}
impl FromStr for TenantProtocolPolicyProperty {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
TenantProtocolPolicyProperty::parse(s).ok_or(())
}
}
impl Element for TenantProtocolPolicyValue {
type Property = TenantProtocolPolicyProperty;
fn try_parse<P>(key: &Key<'_, Self::Property>, value: &str) -> Option<Self> {
match key {
Key::Property(TenantProtocolPolicyProperty::Id) => {
Id::from_str(value).ok().map(TenantProtocolPolicyValue::Id)
}
_ => None,
}
}
fn to_cow(&self) -> Cow<'static, str> {
match self {
TenantProtocolPolicyValue::Id(id) => id.to_string().into(),
}
}
}
impl JmapObject for TenantProtocolPolicy {
type Property = TenantProtocolPolicyProperty;
type Element = TenantProtocolPolicyValue;
type Id = Id;
type Filter = ();
type Comparator = ();
type GetArguments = ();
type SetArguments<'de> = ();
type QueryArguments = ();
type CopyArguments = ();
type ParseArguments = ();
const ID_PROPERTY: Self::Property = TenantProtocolPolicyProperty::Id;
}
impl From<Id> for TenantProtocolPolicyValue {
fn from(id: Id) -> Self {
TenantProtocolPolicyValue::Id(id)
}
}
impl JmapObjectId for TenantProtocolPolicyValue {
fn as_id(&self) -> Option<Id> {
match self {
TenantProtocolPolicyValue::Id(id) => Some(*id),
}
}
fn as_any_id(&self) -> Option<AnyId> {
match self {
TenantProtocolPolicyValue::Id(id) => Some(AnyId::Id(*id)),
}
}
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 = TenantProtocolPolicyValue::Id(id);
true
} else {
false
}
}
}
impl JmapObjectId for TenantProtocolPolicyProperty {
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
}
}