The urn:inbuxa:jmap capability on the signed-in principal's own account gains legacyProtocols: "enabled" or "disabled", the stricter of the server's switch and the account's tenant's (legacy-protocols spec, Interfaces). It is what the webmail needs to tell someone why their phone's mail app won't connect (LP-19), and it closes acceptance test 13. contract.md's C-1 gains the line. It is an optional field added, which C-3 says doesn't bump the contract version. tests/e2e/legacy_protocols.py reads it back from the session on a running server: enabled for the tenant's user while both switches are on, disabled once its tenant turns legacy protocols off while an account outside the tenant still reads enabled, disabled for everyone while the server switch is off, and enabled again at the end. All 67 checks pass.
177 lines
6.9 KiB
Rust
177 lines
6.9 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
use common::{Server, auth::AccessToken};
|
|
use jmap_proto::request::capability::{
|
|
Account, Capabilities, Capability, EmptyCapabilities, InbuxaAccountCapabilities, Session,
|
|
};
|
|
use registry::schema::enums::Permission;
|
|
use std::future::Future;
|
|
use trc::AddContext;
|
|
use types::id::Id;
|
|
use utils::map::vec_map::VecMap;
|
|
|
|
pub trait SessionHandler: Sync + Send {
|
|
fn handle_session_resource(
|
|
&self,
|
|
base_url: String,
|
|
access_token: &AccessToken,
|
|
) -> impl Future<Output = trc::Result<Session>> + Send;
|
|
}
|
|
|
|
impl SessionHandler for Server {
|
|
async fn handle_session_resource(
|
|
&self,
|
|
base_url: String,
|
|
access_token: &AccessToken,
|
|
) -> trc::Result<Session> {
|
|
let mut session = Session::new(base_url, &self.core.jmap.capabilities);
|
|
session.set_state(access_token.state());
|
|
let account_capabilities = &self.core.jmap.capabilities.account;
|
|
|
|
// Set primary account
|
|
let account = self
|
|
.account(access_token.account_id())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
session.username = account.name().to_string();
|
|
let account_id = Id::from(access_token.account_id());
|
|
let mut account = Account {
|
|
name: account.name().to_string(),
|
|
is_personal: true,
|
|
is_read_only: false,
|
|
account_capabilities: VecMap::with_capacity(account_capabilities.len()),
|
|
};
|
|
for capability in access_token.account_capabilities() {
|
|
session.primary_accounts.append(capability, account_id);
|
|
account.account_capabilities.append(
|
|
capability,
|
|
account_capabilities
|
|
.get(&capability)
|
|
.map(|v| v.to_account_capabilities(account_id.into(), true))
|
|
.unwrap_or_else(|| Capabilities::Empty(EmptyCapabilities::default())),
|
|
);
|
|
}
|
|
// inbuxa: MT-22: the logo that applies to the signed-in principal
|
|
let logo =
|
|
inbuxa_features::tenancy::logo::for_account(self.registry(), access_token.account_id())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
session.capabilities.append(
|
|
Capability::Inbuxa,
|
|
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(
|
|
Capability::Inbuxa,
|
|
Capabilities::Inbuxa(InbuxaAccountCapabilities {
|
|
logo,
|
|
legacy_protocols,
|
|
}),
|
|
);
|
|
// inbuxa: Fastmail's Masked Email API, for accounts that may hold masks
|
|
if access_token.has_permission(Permission::SysMaskedEmailGet) {
|
|
session.capabilities.append(
|
|
Capability::FastmailMaskedEmail,
|
|
Capabilities::Empty(EmptyCapabilities::default()),
|
|
);
|
|
account.account_capabilities.append(
|
|
Capability::FastmailMaskedEmail,
|
|
Capabilities::Empty(EmptyCapabilities::default()),
|
|
);
|
|
session
|
|
.primary_accounts
|
|
.append(Capability::FastmailMaskedEmail, account_id);
|
|
}
|
|
session.accounts.append(account_id, account);
|
|
|
|
// Add secondary accounts
|
|
for &account_id in access_token.secondary_ids() {
|
|
let is_owner = access_token.is_member(account_id);
|
|
let Some(account) = self
|
|
.try_account(account_id)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
else {
|
|
trc::event!(
|
|
Auth(trc::AuthEvent::Warning),
|
|
AccountId = account_id,
|
|
Reason = "Skipping orphan secondary account id in session",
|
|
);
|
|
continue;
|
|
};
|
|
|
|
let account_id = Id::from(account_id);
|
|
let mut account = Account {
|
|
name: account.name().to_string(),
|
|
is_personal: false,
|
|
is_read_only: false,
|
|
account_capabilities: VecMap::with_capacity(account_capabilities.len()),
|
|
};
|
|
for capability in access_token.account_capabilities() {
|
|
account.account_capabilities.append(
|
|
capability,
|
|
account_capabilities
|
|
.get(&capability)
|
|
.map(|v| v.to_account_capabilities(account_id.into(), is_owner))
|
|
.unwrap_or_else(|| Capabilities::Empty(EmptyCapabilities::default())),
|
|
);
|
|
}
|
|
session.accounts.append(account_id, account);
|
|
}
|
|
|
|
Ok(session)
|
|
}
|
|
}
|
|
|
|
trait AccountCapabilities {
|
|
fn account_capabilities(&self) -> impl Iterator<Item = Capability>;
|
|
}
|
|
|
|
impl AccountCapabilities for AccessToken {
|
|
fn account_capabilities(&self) -> impl Iterator<Item = Capability> {
|
|
Capability::all_capabilities()
|
|
.iter()
|
|
.filter(move |capability| {
|
|
let permission = match capability {
|
|
Capability::Mail | Capability::MailShare | Capability::EmailPush => {
|
|
Permission::JmapEmailGet
|
|
}
|
|
Capability::Submission => Permission::JmapEmailSubmissionCreate,
|
|
Capability::VacationResponse => Permission::JmapVacationResponseGet,
|
|
Capability::Contacts => Permission::JmapContactCardGet,
|
|
Capability::ContactsParse => Permission::JmapContactCardParse,
|
|
Capability::Calendars => Permission::JmapCalendarEventGet,
|
|
Capability::CalendarsParse => Permission::JmapCalendarEventParse,
|
|
Capability::Sieve => Permission::JmapSieveScriptGet,
|
|
Capability::Blob => Permission::JmapBlobGet,
|
|
Capability::Quota => Permission::JmapQuotaGet,
|
|
Capability::FileNode => Permission::JmapFileNodeGet,
|
|
Capability::WebSocket
|
|
| Capability::Principals
|
|
| Capability::PrincipalsAvailability
|
|
| Capability::Stalwart => return true,
|
|
Capability::Core
|
|
| Capability::PrincipalsOwner
|
|
| Capability::WebPushVapid
|
|
| Capability::Inbuxa
|
|
| Capability::FastmailMaskedEmail => {
|
|
return false;
|
|
}
|
|
};
|
|
self.has_permission(permission)
|
|
})
|
|
.copied()
|
|
}
|
|
}
|