Legacy mail apps: tenant switch, session flag, use panel and cleanup #10
@@ -403,6 +403,23 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether legacy protocols are off for this account: the stricter of the
|
||||||
|
/// server's switch and its tenant's. What the JMAP session tells the
|
||||||
|
/// account's apps (legacy-protocols spec, Interfaces), so the webmail can
|
||||||
|
/// say why a mail app won't connect (LP-19).
|
||||||
|
pub async fn legacy_protocols_off_for_account(
|
||||||
|
&self,
|
||||||
|
access_token: &AccessToken,
|
||||||
|
) -> trc::Result<bool> {
|
||||||
|
if self.protocol_policy().await?.legacy_protocols.is_disabled() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
match access_token.tenant_id() {
|
||||||
|
Some(tenant_id) => self.tenant_legacy_protocols_off(tenant_id).await,
|
||||||
|
None => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a tenant has turned legacy protocols off for itself (LP-10).
|
/// Whether a tenant has turned legacy protocols off for itself (LP-10).
|
||||||
pub async fn tenant_legacy_protocols_off(&self, tenant_id: u32) -> trc::Result<bool> {
|
pub async fn tenant_legacy_protocols_off(&self, tenant_id: u32) -> trc::Result<bool> {
|
||||||
Ok(
|
Ok(
|
||||||
|
|||||||
@@ -142,6 +142,11 @@ pub struct InbuxaAccountCapabilities {
|
|||||||
/// The logo that applies to the principal (MT-22): a URL or a data URL.
|
/// The logo that applies to the principal (MT-22): a URL or a data URL.
|
||||||
#[serde(rename(serialize = "logo"))]
|
#[serde(rename(serialize = "logo"))]
|
||||||
pub logo: Option<String>,
|
pub logo: Option<String>,
|
||||||
|
/// Whether legacy mail protocols are `enabled` or `disabled` for the
|
||||||
|
/// principal: the stricter of the server's switch and its tenant's
|
||||||
|
/// (legacy-protocols spec, Interfaces; LP-19).
|
||||||
|
#[serde(rename(serialize = "legacyProtocols"))]
|
||||||
|
pub legacy_protocols: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
|||||||
@@ -66,9 +66,18 @@ impl SessionHandler for Server {
|
|||||||
Capability::Inbuxa,
|
Capability::Inbuxa,
|
||||||
Capabilities::Empty(EmptyCapabilities::default()),
|
Capabilities::Empty(EmptyCapabilities::default()),
|
||||||
);
|
);
|
||||||
|
// inbuxa: legacy-protocols, Interfaces: whichever switch is stricter
|
||||||
|
let legacy_protocols = if self.legacy_protocols_off_for_account(access_token).await? {
|
||||||
|
"disabled"
|
||||||
|
} else {
|
||||||
|
"enabled"
|
||||||
|
};
|
||||||
account.account_capabilities.append(
|
account.account_capabilities.append(
|
||||||
Capability::Inbuxa,
|
Capability::Inbuxa,
|
||||||
Capabilities::Inbuxa(InbuxaAccountCapabilities { logo }),
|
Capabilities::Inbuxa(InbuxaAccountCapabilities {
|
||||||
|
logo,
|
||||||
|
legacy_protocols,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
// inbuxa: Fastmail's Masked Email API, for accounts that may hold masks
|
// inbuxa: Fastmail's Masked Email API, for accounts that may hold masks
|
||||||
if access_token.has_permission(Permission::SysMaskedEmailGet) {
|
if access_token.has_permission(Permission::SysMaskedEmailGet) {
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ Each has an ID, and tests name the IDs they check.
|
|||||||
In `accountCapabilities`, the signed-in principal's own account carries
|
In `accountCapabilities`, the signed-in principal's own account carries
|
||||||
`urn:inbuxa:jmap` with `logo`: the logo that applies to it (multi-tenancy
|
`urn:inbuxa:jmap` with `logo`: the logo that applies to it (multi-tenancy
|
||||||
MT-22), a string (URL or data URL) or `null`. Added 2026-09-18.
|
MT-22), a string (URL or data URL) or `null`. Added 2026-09-18.
|
||||||
|
|
||||||
|
It also carries `legacyProtocols`: `enabled` or `disabled`, whether IMAP,
|
||||||
|
POP3, ManageSieve and SMTP submission are off for the principal -- the
|
||||||
|
stricter of the server's switch and its tenant's (legacy-protocols spec,
|
||||||
|
Interfaces). A front end uses it to say why a mail app can't connect
|
||||||
|
(LP-19). Added 2026-09-21.
|
||||||
- **C-2.** Each front end states the contract versions it supports and checks
|
- **C-2.** Each front end states the contract versions it supports and checks
|
||||||
`contract` after signing in. Outside its range it stops, with a message
|
`contract` after signing in. Outside its range it stops, with a message
|
||||||
naming both versions. For ihasmail-inbuxa this replaces public ihasmail's
|
naming both versions. For ihasmail-inbuxa this replaces public ihasmail's
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ address or made-up, right password or wrong -- in the organization's words,
|
|||||||
leaves every other domain alone, and stops client configuration offering
|
leaves every other domain alone, and stops client configuration offering
|
||||||
legacy servers for those domains. It reaches only its own tenant's switch,
|
legacy servers for those domains. It reaches only its own tenant's switch,
|
||||||
and can't turn it back on while the server has legacy protocols off
|
and can't turn it back on while the server has legacy protocols off
|
||||||
(acceptance tests 6 to 10, 14).
|
(acceptance tests 6 to 10, 14). Throughout, the JMAP session tells each
|
||||||
|
account which way its switches point (test 13).
|
||||||
|
|
||||||
Passwords are generated into files under target/e2e and never printed.
|
Passwords are generated into files under target/e2e and never printed.
|
||||||
Everything is removed afterwards unless KEEP=1.
|
Everything is removed afterwards unless KEEP=1.
|
||||||
@@ -289,6 +290,9 @@ def tenant_checks(admin, admin_pw, account):
|
|||||||
tset = lambda value: one(ta, tadmin_pw, "inbuxa:TenantProtocolPolicy/set",
|
tset = lambda value: one(ta, tadmin_pw, "inbuxa:TenantProtocolPolicy/set",
|
||||||
{"accountId": tacct, "update": {t: {"legacyProtocols": value}}})
|
{"accountId": tacct, "update": {t: {"legacyProtocols": value}}})
|
||||||
|
|
||||||
|
check(session_flag(tu, user_pw) == "enabled",
|
||||||
|
"the session says enabled for the tenant's user while both switches are on (test 13)")
|
||||||
|
|
||||||
# Before: the tenant's user signs in, and its domain is offered IMAP.
|
# Before: the tenant's user signs in, and its domain is offered IMAP.
|
||||||
check(imap_login(PORTS["imap"], tu, user_pw).startswith("OK"),
|
check(imap_login(PORTS["imap"], tu, user_pw).startswith("OK"),
|
||||||
"a tenant's user signs in over IMAP with the tenant's switch on")
|
"a tenant's user signs in over IMAP with the tenant's switch on")
|
||||||
@@ -313,6 +317,11 @@ def tenant_checks(admin, admin_pw, account):
|
|||||||
'value = "disabled"'),
|
'value = "disabled"'),
|
||||||
"and it is an event, scope tenant (LP-14, test 14)")
|
"and it is an event, scope tenant (LP-14, test 14)")
|
||||||
|
|
||||||
|
check(session_flag(tu, user_pw) == "disabled",
|
||||||
|
"the session says disabled for the tenant's user once its tenant turns it off (test 13)")
|
||||||
|
check(session_flag(admin, admin_pw) == "enabled",
|
||||||
|
"and still enabled for an account outside the tenant (test 13)")
|
||||||
|
|
||||||
# Refused on the tenant's domain, every way in the same words (tests 6-8).
|
# Refused on the tenant's domain, every way in the same words (tests 6-8).
|
||||||
imap_no = ("NO [ALERT] Your organization allows only INBUXA webmail and JMAP apps. "
|
imap_no = ("NO [ALERT] Your organization allows only INBUXA webmail and JMAP apps. "
|
||||||
"This mail app can't sign in.")
|
"This mail app can't sign in.")
|
||||||
@@ -343,6 +352,8 @@ def tenant_checks(admin, admin_pw, account):
|
|||||||
# Server off means off for everyone: the tenant can't turn it back on (test 9).
|
# Server off means off for everyone: the tenant can't turn it back on (test 9).
|
||||||
one(admin, admin_pw, "inbuxa:ProtocolPolicy/set",
|
one(admin, admin_pw, "inbuxa:ProtocolPolicy/set",
|
||||||
{"accountId": account, "update": {"singleton": {"legacyProtocols": "disabled"}}})
|
{"accountId": account, "update": {"singleton": {"legacyProtocols": "disabled"}}})
|
||||||
|
check(session_flag(admin, admin_pw) == "disabled",
|
||||||
|
"with the server off, the session says disabled for everyone (test 13)")
|
||||||
res = tset("enabled")
|
res = tset("enabled")
|
||||||
refused = (res[1].get("notUpdated") or {}).get(t) or {}
|
refused = (res[1].get("notUpdated") or {}).get(t) or {}
|
||||||
check(refused.get("type") == "forbidden"
|
check(refused.get("type") == "forbidden"
|
||||||
@@ -357,6 +368,14 @@ def tenant_checks(admin, admin_pw, account):
|
|||||||
check(t in (res[1].get("updated") or {}), "with the server on, the tenant turns them back on")
|
check(t in (res[1].get("updated") or {}), "with the server on, the tenant turns them back on")
|
||||||
check(imap_login(PORTS["imap"], tu, user_pw).startswith("OK"),
|
check(imap_login(PORTS["imap"], tu, user_pw).startswith("OK"),
|
||||||
"and its user signs in over IMAP again")
|
"and its user signs in over IMAP again")
|
||||||
|
check(session_flag(tu, user_pw) == "enabled", "and its session says enabled again (test 13)")
|
||||||
|
|
||||||
|
|
||||||
|
def session_flag(user, password):
|
||||||
|
"""legacyProtocols from the account's urn:inbuxa:jmap capability."""
|
||||||
|
sess = session(user, password)
|
||||||
|
acct = sess["primaryAccounts"].get(INBUXA) or list(sess["accounts"])[0]
|
||||||
|
return sess["accounts"][acct]["accountCapabilities"].get(INBUXA, {}).get("legacyProtocols")
|
||||||
|
|
||||||
|
|
||||||
def events_matching(name, *parts):
|
def events_matching(name, *parts):
|
||||||
|
|||||||
Reference in New Issue
Block a user