The switch reports every change as an event (LP-8)

Turning legacy mail protocols off or back on raises
security.legacy-protocols-changed (id 643, info level, also in the packaged
schema), with the scope (policy = server), the new value, who made the
change (accountId), whether listeners closed or reopened (details), which
ones (listenerId), and -- only when a listener could not be put back --
which and why (reason).

It is raised in Server::set_protocol_policy rather than by the JMAP
method, so whatever turns the switch is reported. A /set that changes
nothing -- the switch already where it was asked to be, nothing to close
or reopen -- is not a change and raises nothing.

The event is never an error, but jmap's exhaustive map from security
events to HTTP errors has to name it; it joins the other two that can't
occur there. rustfmt now also wraps LP-6's two over-long lines in
enums_impl.rs, which it flagged along with this change's.

tests/e2e/legacy_protocols.py now gives the server a stdout tracer and
reads events from the container's log: turning the switch off is exactly
one event naming the scope, value, author and listeners closed; setting it
off again raises none; turning it on is one event naming the listeners
reopened. It also proves LP-6's side: seven refused submission sign-ins
are seven auth.legacy-protocol-refused events, and there is no auth.failed
or auth.too-many-attempts among them. All checks pass.
This commit is contained in:
2026-09-21 10:53:01 -07:00
parent 4b585905d7
commit 64cddc9246
7 changed files with 118 additions and 6 deletions
+37
View File
@@ -107,6 +107,37 @@ impl Server {
protocol_policy::set(&self.core.storage.data, &policy).await?;
// LP-8. Raised here rather than by the JMAP method, so whatever turns
// the switch is reported. A /set that changed nothing -- the switch
// already where it was asked to be, nothing to close or reopen -- is
// not a change.
if previous.legacy_protocols != policy.legacy_protocols || !change.is_empty() {
let (moved, direction) = if policy.legacy_protocols.is_disabled() {
(&change.closed, "closed")
} else {
(&change.reopened, "reopened")
};
trc::event!(
Security(trc::SecurityEvent::LegacyProtocolsChanged),
Policy = "server",
Value = if policy.legacy_protocols.is_disabled() {
"disabled"
} else {
"enabled"
},
AccountId = policy.changed_by.clone(),
Details = direction,
ListenerId = listener_names(moved.iter().map(|l| l.id.clone())),
// Only when a listener could not be put back (LP-5).
Reason = (!change.failed.is_empty()).then(|| listener_names(
change
.failed
.iter()
.map(|(l, why)| format!("{}: {why}", l.id))
)),
);
}
Ok(change)
}
@@ -220,6 +251,12 @@ impl Server {
}
}
/// Names for an event field: the listeners a change closed, reopened or
/// failed to reopen (LP-8).
fn listener_names<T: Into<trc::Value>>(names: impl Iterator<Item = T>) -> trc::Value {
trc::Value::Array(names.map(Into::into).collect())
}
/// A protocol a mail app signs in over, which the switch refuses (LP-6).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegacyProtocol {
+6 -1
View File
@@ -2,6 +2,8 @@
* 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 crate::blob::UploadResponse;
@@ -186,7 +188,10 @@ impl ToRequestError for trc::Error {
trc::SecurityEvent::Unauthorized | trc::SecurityEvent::IpUnauthorized => {
RequestError::forbidden()
}
trc::SecurityEvent::IpBlockExpired | trc::SecurityEvent::IpAllowExpired => {
// inbuxa: legacy-protocols LP-8 is an event, never an error
trc::SecurityEvent::IpBlockExpired
| trc::SecurityEvent::IpAllowExpired
| trc::SecurityEvent::LegacyProtocolsChanged => {
RequestError::internal_server_error()
}
},
+5 -2
View File
@@ -9,8 +9,9 @@
// This file is auto-generated. Do not edit directly.
// inbuxa: 637 to 641 are the fork's SCIM events (SCIM-54); 642 is
// auth.legacy-protocol-refused (legacy-protocols LP-6)
pub const TOTAL_EVENT_COUNT: usize = 643;
// auth.legacy-protocol-refused (legacy-protocols LP-6); 643 is
// security.legacy-protocols-changed (LP-8)
pub const TOTAL_EVENT_COUNT: usize = 644;
pub const TOTAL_METRIC_COUNT: usize = 369;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -655,6 +656,8 @@ pub enum SecurityEvent {
IpAllowExpired = 594,
IpUnauthorized = 279,
Unauthorized = 552,
// inbuxa: legacy-protocols LP-8
LegacyProtocolsChanged = 643,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+28 -2
View File
@@ -446,6 +446,8 @@ impl EventType {
b"security.ip-allow-expired" => EventType::Security(SecurityEvent::IpAllowExpired),
b"security.ip-unauthorized" => EventType::Security(SecurityEvent::IpUnauthorized),
b"security.unauthorized" => EventType::Security(SecurityEvent::Unauthorized),
// inbuxa: legacy-protocols LP-8
b"security.legacy-protocols-changed" => EventType::Security(SecurityEvent::LegacyProtocolsChanged),
b"server.startup" => EventType::Server(ServerEvent::Startup),
b"server.shutdown" => EventType::Server(ServerEvent::Shutdown),
b"server.startup-error" => EventType::Server(ServerEvent::StartupError),
@@ -1211,6 +1213,10 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => "security.ip-allow-expired",
EventType::Security(SecurityEvent::IpUnauthorized) => "security.ip-unauthorized",
EventType::Security(SecurityEvent::Unauthorized) => "security.unauthorized",
// inbuxa: legacy-protocols LP-8
EventType::Security(SecurityEvent::LegacyProtocolsChanged) => {
"security.legacy-protocols-changed"
}
EventType::Server(ServerEvent::Startup) => "server.startup",
EventType::Server(ServerEvent::Shutdown) => "server.shutdown",
EventType::Server(ServerEvent::StartupError) => "server.startup-error",
@@ -1883,6 +1889,8 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => 594,
EventType::Security(SecurityEvent::IpUnauthorized) => 279,
EventType::Security(SecurityEvent::Unauthorized) => 552,
// inbuxa: legacy-protocols LP-8
EventType::Security(SecurityEvent::LegacyProtocolsChanged) => 643,
EventType::Server(ServerEvent::Startup) => 393,
EventType::Server(ServerEvent::Shutdown) => 392,
EventType::Server(ServerEvent::StartupError) => 394,
@@ -2571,6 +2579,8 @@ impl EventType {
594 => Some(EventType::Security(SecurityEvent::IpAllowExpired)),
279 => Some(EventType::Security(SecurityEvent::IpUnauthorized)),
552 => Some(EventType::Security(SecurityEvent::Unauthorized)),
// inbuxa: legacy-protocols LP-8
643 => Some(EventType::Security(SecurityEvent::LegacyProtocolsChanged)),
393 => Some(EventType::Server(ServerEvent::Startup)),
392 => Some(EventType::Server(ServerEvent::Shutdown)),
394 => Some(EventType::Server(ServerEvent::StartupError)),
@@ -2990,6 +3000,8 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => Level::Info,
EventType::Security(SecurityEvent::IpUnauthorized) => Level::Info,
EventType::Security(SecurityEvent::Unauthorized) => Level::Info,
// inbuxa: legacy-protocols LP-8
EventType::Security(SecurityEvent::LegacyProtocolsChanged) => Level::Info,
EventType::Server(ServerEvent::Startup) => Level::Info,
EventType::Server(ServerEvent::Shutdown) => Level::Info,
EventType::Server(ServerEvent::Licensing) => Level::Info,
@@ -3198,7 +3210,9 @@ impl EventType {
EventType::Auth(AuthEvent::TooManyAttempts) => "Too many authentication attempts",
EventType::Auth(AuthEvent::ClientRegistration) => "OAuth Client registration",
// inbuxa: legacy-protocols LP-6
EventType::Auth(AuthEvent::LegacyProtocolRefused) => "Legacy mail protocol sign-in refused",
EventType::Auth(AuthEvent::LegacyProtocolRefused) => {
"Legacy mail protocol sign-in refused"
}
EventType::Auth(AuthEvent::Error) => "Authentication error",
EventType::Auth(AuthEvent::Warning) => "Authentication warning",
EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired",
@@ -3711,6 +3725,10 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => "IP allow expired",
EventType::Security(SecurityEvent::IpUnauthorized) => "Unauthorized IP address",
EventType::Security(SecurityEvent::Unauthorized) => "Unauthorized access",
// inbuxa: legacy-protocols LP-8
EventType::Security(SecurityEvent::LegacyProtocolsChanged) => {
"Legacy mail protocols switch changed"
}
EventType::Server(ServerEvent::Startup) => "Starting INBUXA Server",
EventType::Server(ServerEvent::Shutdown) => "Shutting down INBUXA Server",
EventType::Server(ServerEvent::StartupError) => "Server startup error",
@@ -3964,7 +3982,9 @@ impl EventType {
EventType::Auth(AuthEvent::TooManyAttempts) => "Too many authentication attempts",
EventType::Auth(AuthEvent::ClientRegistration) => "Authentication error",
// inbuxa: legacy-protocols LP-6
EventType::Auth(AuthEvent::LegacyProtocolRefused) => "This server allows only INBUXA webmail and JMAP apps",
EventType::Auth(AuthEvent::LegacyProtocolRefused) => {
"This server allows only INBUXA webmail and JMAP apps"
}
EventType::Auth(AuthEvent::Error) => "Authentication error",
EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired",
EventType::Imap(ImapEvent::ConnectionStart) => "IMAP error",
@@ -4102,6 +4122,10 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => "Insufficient permissions",
EventType::Security(SecurityEvent::IpUnauthorized) => "Unauthorized IP address",
EventType::Security(SecurityEvent::Unauthorized) => "Insufficient permissions",
// inbuxa: legacy-protocols LP-8
EventType::Security(SecurityEvent::LegacyProtocolsChanged) => {
"Legacy mail protocols switch changed"
}
EventType::Smtp(SmtpEvent::ConnectionStart) => "SMTP error",
EventType::Smtp(SmtpEvent::ConnectionEnd) => "SMTP error",
EventType::Smtp(SmtpEvent::Error) => "SMTP error",
@@ -4663,6 +4687,8 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired),
EventType::Security(SecurityEvent::IpUnauthorized),
EventType::Security(SecurityEvent::Unauthorized),
// inbuxa: legacy-protocols LP-8
EventType::Security(SecurityEvent::LegacyProtocolsChanged),
EventType::Server(ServerEvent::Startup),
EventType::Server(ServerEvent::Shutdown),
EventType::Server(ServerEvent::StartupError),