Author SHA1 Message Date
jcoffey-dev 825f49671e Merge branch 'ci/gitea-actions' into 'main'
ci / build (push) Successful in 37m28s
ci: add Gitea Actions workflow

See merge request inbuxa/inbuxa-server!5
2026-09-22 00:01:05 -07:00
jcoffey-dev 29bcfecb80 ci: add Gitea Actions workflow ported from .gitlab-ci.yml
ci / build (pull_request) Successful in 24m51s
2026-09-21 22:45:05 -07:00
13 changed files with 67 additions and 313 deletions
+42
View File
@@ -0,0 +1,42 @@
# CI on the self-hosted Gitea, ported from .gitlab-ci.yml during the move off
# GitLab (2026-09-22). Gitea reads .gitea/workflows and ignores .github/ once
# this directory exists; .github/workflows stays as it was for GitHub.
#
# Every job runs in an image pinned by digest (tag in the trailing comment),
# and the only action used is coffey-labs/actions/checkout pinned by SHA. The
# instance resolves short `uses:` against itself, never GitHub, so nothing
# unreviewed can be pulled in.
#
# Not ported, as on GitLab: publish.yml and release.yml still need doing.
name: ci
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: docker
container:
image: rust:1-bookworm@sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e # 1-bookworm
# Both kept inside the workspace (a per-job volume on the project disk),
# deliberately not on /tmp, which on this host is a tmpfs that a Rust
# build of this size has filled before. There is no cache between runs
# here -- the runner's cache server is off -- so every build is cold.
env:
CARGO_INCREMENTAL: "0"
steps:
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
- run: |
echo "CARGO_HOME=$GITHUB_WORKSPACE/.cargo" >> "$GITHUB_ENV"
echo "CARGO_TARGET_DIR=$GITHUB_WORKSPACE/target" >> "$GITHUB_ENV"
- run: apt-get update -qq && apt-get install -y -qq --no-install-recommends clang >/dev/null
- run: cargo build -p inbuxa --locked
# --no-run: the workflow compiled every test target without running them,
# which catches a test that no longer builds without paying for the suite.
- run: cargo test --workspace --locked --no-run
+1
View File
@@ -10,6 +10,7 @@ run.sh
!.gitattributes !.gitattributes
!.github !.github
!.gitlab-ci.yml !.gitlab-ci.yml
!.gitea
CLAUDE.md CLAUDE.md
# The cutover rehearsal writes its fixture and state here. # The cutover rehearsal writes its fixture and state here.
+6 -25
View File
@@ -47,9 +47,6 @@ pub struct Network {
#[derive(Clone)] #[derive(Clone)]
pub struct NetworkInfo { pub struct NetworkInfo {
pub pacc: Pacc, pub pacc: Pacc,
/// inbuxa: the same document without IMAP, POP3, SMTP and ManageSieve,
/// served while legacy protocols are off (legacy-protocols LP-7).
pub pacc_jmap_only: Pacc,
pub mxs: Vec<MailExchanger>, pub mxs: Vec<MailExchanger>,
pub services: VecMap<ServiceProtocol, Service>, pub services: VecMap<ServiceProtocol, Service>,
} }
@@ -323,26 +320,11 @@ impl Network {
} }
} }
let split = |pacc: &Configuration| { let (prefix, suffix) = serde_json::to_string(&pacc)
serde_json::to_string(pacc) .unwrap_or_default()
.unwrap_or_default() .rsplit_once(SPLIT_HERE)
.rsplit_once(SPLIT_HERE) .map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string()))
.map(|(prefix, suffix)| Pacc { .unwrap();
prefix: prefix.to_string(),
suffix: suffix.to_string(),
})
.unwrap()
};
// inbuxa: legacy-protocols LP-7
let pacc_jmap_only = {
let mut pacc = pacc.clone();
pacc.protocols.imap = None;
pacc.protocols.pop3 = None;
pacc.protocols.smtp = None;
pacc.protocols.managesieve = None;
split(&pacc)
};
let pacc = split(&pacc);
let mut network = Network { let mut network = Network {
node_id: bp.node_id() as u64, node_id: bp.node_id() as u64,
server_name: default_hostname.to_string(), server_name: default_hostname.to_string(),
@@ -357,8 +339,7 @@ impl Network {
info: NetworkInfo { info: NetworkInfo {
mxs: system.mail_exchangers.into_iter().collect(), mxs: system.mail_exchangers.into_iter().collect(),
services: system.services, services: system.services,
pacc, pacc: Pacc { prefix, suffix },
pacc_jmap_only,
}, },
}; };
@@ -2,11 +2,9 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::{Server, manager::application::Resource, network::legacy::is_legacy_service}; use crate::{Server, manager::application::Resource};
use quick_xml::Reader; use quick_xml::Reader;
use quick_xml::XmlVersion; use quick_xml::XmlVersion;
use quick_xml::events::Event; use quick_xml::events::Event;
@@ -57,12 +55,7 @@ impl Server {
let _ = writeln!(&mut config, "\t\t<Account>"); let _ = writeln!(&mut config, "\t\t<Account>");
let _ = writeln!(&mut config, "\t\t\t<AccountType>email</AccountType>"); let _ = writeln!(&mut config, "\t\t\t<AccountType>email</AccountType>");
let _ = writeln!(&mut config, "\t\t\t<Action>settings</Action>"); let _ = writeln!(&mut config, "\t\t\t<Action>settings</Action>");
// inbuxa: legacy-protocols LP-7
let legacy_off = self.legacy_protocols_off().await?;
for (protocol, service) in &self.core.network.info.services { for (protocol, service) in &self.core.network.info.services {
if legacy_off && is_legacy_service(protocol) {
continue;
}
let (protocol, ports) = match protocol { let (protocol, ports) = match protocol {
ServiceProtocol::Imap => ("IMAP", [143, 993]), ServiceProtocol::Imap => ("IMAP", [143, 993]),
ServiceProtocol::Pop3 => ("POP3", [110, 995]), ServiceProtocol::Pop3 => ("POP3", [110, 995]),
@@ -2,11 +2,9 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::{Server, manager::application::Resource, network::legacy::is_legacy_service}; use crate::{Server, manager::application::Resource};
use registry::schema::enums::ServiceProtocol; use registry::schema::enums::ServiceProtocol;
use std::fmt::Write; use std::fmt::Write;
use utils::url_params::UrlParams; use utils::url_params::UrlParams;
@@ -30,9 +28,6 @@ impl Server {
("%EMAILADDRESS%", default_host.as_str()) ("%EMAILADDRESS%", default_host.as_str())
}; };
// inbuxa: legacy-protocols LP-7
let legacy_off = self.legacy_protocols_off().await?;
// Build XML response // Build XML response
let mut config = String::with_capacity(1024); let mut config = String::with_capacity(1024);
config.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); config.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
@@ -45,9 +40,6 @@ impl Server {
"\t\t<displayShortName>{domain}</displayShortName>" "\t\t<displayShortName>{domain}</displayShortName>"
); );
for (protocol, service) in &self.core.network.info.services { for (protocol, service) in &self.core.network.info.services {
if legacy_off && is_legacy_service(protocol) {
continue;
}
let (protocol, tag, ports) = match protocol { let (protocol, tag, ports) = match protocol {
ServiceProtocol::Smtp => ("smtp", "outgoingServer", [587, 465]), ServiceProtocol::Smtp => ("smtp", "outgoingServer", [587, 465]),
ServiceProtocol::Imap => ("imap", "incomingServer", [143, 993]), ServiceProtocol::Imap => ("imap", "incomingServer", [143, 993]),
+9 -44
View File
@@ -2,15 +2,9 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::{ use crate::{Server, config::network::Pacc, network::dkim::generate_dkim_dns_record};
Server,
config::network::Pacc,
network::{dkim::generate_dkim_dns_record, legacy::is_legacy_service},
};
use ahash::{AHashMap, AHashSet}; use ahash::{AHashMap, AHashSet};
use base64::{Engine, engine::general_purpose}; use base64::{Engine, engine::general_purpose};
use dns_update::{ use dns_update::{
@@ -39,8 +33,6 @@ impl Server {
let mut records = Vec::new(); let mut records = Vec::new();
let network = &self.core.network; let network = &self.core.network;
let default_host = network.server_name.as_str(); let default_host = network.server_name.as_str();
// inbuxa: legacy-protocols LP-7
let legacy_off = self.legacy_protocols_off().await?;
let domain_name = domain.name.as_str(); let domain_name = domain.name.as_str();
let domain_name_suffix = format!(".{domain_name}"); let domain_name_suffix = format!(".{domain_name}");
@@ -201,25 +193,6 @@ impl Server {
ServiceProtocol::Smtp => [("submission", 587), ("submissions", 465)], ServiceProtocol::Smtp => [("submission", 587), ("submissions", 465)],
}; };
// inbuxa: legacy-protocols LP-7. While they are off, every
// name says "not offered" -- target "." (RFC 6186 section
// 3.4) -- rather than vanishing, so a client that looks
// is told, and an old record left in the zone is replaced.
if legacy_off && is_legacy_service(protocol) {
for (service_name, _) in services {
records.push(NamedDnsRecord {
name: format!("_{service_name}._tcp.{domain_name}."),
record: DnsRecord::SRV(SRVRecord {
target: ".".to_string(),
priority: 0,
weight: 0,
port: 0,
}),
});
}
continue;
}
for (is_tls, (service_name, port)) in services.into_iter().enumerate() { for (is_tls, (service_name, port)) in services.into_iter().enumerate() {
if is_tls == 1 || service.cleartext { if is_tls == 1 || service.cleartext {
records.push(NamedDnsRecord { records.push(NamedDnsRecord {
@@ -304,14 +277,6 @@ impl Server {
for (protocol, service) in &network.info.services { for (protocol, service) in &network.info.services {
let hostname = service.hostname.as_deref().unwrap_or(default_host); let hostname = service.hostname.as_deref().unwrap_or(default_host);
if hostname.ends_with(&domain_name_suffix) || hostname == domain_name { if hostname.ends_with(&domain_name_suffix) || hostname == domain_name {
// inbuxa: legacy-protocols LP-7. No TLS pin for a port
// the switch has closed. Submission's port stays open
// (the SMTP lock), so its record stays.
if legacy_off
&& matches!(protocol, ServiceProtocol::Imap | ServiceProtocol::Pop3)
{
continue;
}
let port = match protocol { let port = match protocol {
ServiceProtocol::Imap => 993, ServiceProtocol::Imap => 993,
ServiceProtocol::Pop3 => 995, ServiceProtocol::Pop3 => 995,
@@ -417,12 +382,6 @@ impl Server {
} }
pub async fn get_pacc_for_domain(&self, domain_name: &str) -> trc::Result<String> { pub async fn get_pacc_for_domain(&self, domain_name: &str) -> trc::Result<String> {
// inbuxa: legacy-protocols LP-7
let pacc = if self.legacy_protocols_off().await? {
&self.core.network.info.pacc_jmap_only
} else {
&self.core.network.info.pacc
};
self.get_directory_for_domain(domain_name) self.get_directory_for_domain(domain_name)
.await .await
.caused_by(trc::location!()) .caused_by(trc::location!())
@@ -431,9 +390,15 @@ impl Server {
.and_then(|directory| { .and_then(|directory| {
directory directory
.oidc_discovery_document() .oidc_discovery_document()
.map(|doc| pacc.build(&doc.url)) .map(|doc| self.core.network.info.pacc.build(&doc.url))
})
.unwrap_or_else(|| {
self.core
.network
.info
.pacc
.build(&self.core.network.http.url_https)
}) })
.unwrap_or_else(|| pacc.build(&self.core.network.http.url_https))
}) })
} }
} }
-83
View File
@@ -21,10 +21,6 @@
//! a legacy protocol is refused before any password is looked at, so a //! a legacy protocol is refused before any password is looked at, so a
//! listener that exists by mistake still lets nobody in. //! listener that exists by mistake still lets nobody in.
//! //!
//! And nothing advertises what is closed (LP-7): client configuration and
//! the suggested DNS records leave the legacy services out, or mark them as
//! not offered, while the switch is off.
//!
//! Nothing here touches the host's firewall, NAT port-forwards or any proxy //! Nothing here touches the host's firewall, NAT port-forwards or any proxy
//! (LP-20). The server stops answering; what still routes the port is the //! (LP-20). The server stops answering; what still routes the port is the
//! operator's to reconcile. //! operator's to reconcile.
@@ -35,7 +31,6 @@ use inbuxa_features::security::{
listeners, listeners,
protocol_policy::{self, ProtocolPolicy, SavedListener}, protocol_policy::{self, ProtocolPolicy, SavedListener},
}; };
use registry::schema::enums::ServiceProtocol;
use registry::types::{error::Error, id::ObjectId}; use registry::types::{error::Error, id::ObjectId};
use store::registry::bootstrap::Bootstrap; use store::registry::bootstrap::Bootstrap;
@@ -107,37 +102,6 @@ impl Server {
protocol_policy::set(&self.core.storage.data, &policy).await?; 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) Ok(change)
} }
@@ -251,12 +215,6 @@ 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). /// A protocol a mail app signs in over, which the switch refuses (LP-6).
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegacyProtocol { pub enum LegacyProtocol {
@@ -344,27 +302,6 @@ impl Server {
} }
} }
/// The services mail apps sign in to, which the switch turns off: nothing may
/// offer them while it is (LP-7). SMTP here is submission -- mail apps
/// sending -- since inbound mail is never a configured service.
pub fn is_legacy_service(protocol: &ServiceProtocol) -> bool {
matches!(
protocol,
ServiceProtocol::Imap
| ServiceProtocol::Pop3
| ServiceProtocol::Smtp
| ServiceProtocol::Managesieve
)
}
impl Server {
/// Whether the server-wide switch is off, for the answers that must stop
/// offering legacy services (LP-7). Read per answer, as sign-in reads it.
pub async fn legacy_protocols_off(&self) -> trc::Result<bool> {
Ok(self.protocol_policy().await?.legacy_protocols.is_disabled())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -412,26 +349,6 @@ mod tests {
assert_eq!(err.value_as_str(trc::Key::AccountName), None); assert_eq!(err.value_as_str(trc::Key::AccountName), None);
} }
#[test]
fn only_the_services_mail_apps_sign_in_to_are_legacy() {
for protocol in [
ServiceProtocol::Imap,
ServiceProtocol::Pop3,
ServiceProtocol::Smtp,
ServiceProtocol::Managesieve,
] {
assert!(is_legacy_service(&protocol), "{protocol:?}");
}
for protocol in [
ServiceProtocol::Jmap,
ServiceProtocol::Caldav,
ServiceProtocol::Carddav,
ServiceProtocol::Webdav,
] {
assert!(!is_legacy_service(&protocol), "{protocol:?}");
}
}
#[test] #[test]
fn the_domain_comes_from_the_name_given() { fn the_domain_comes_from_the_name_given() {
assert_eq!(domain_of(&basic("[email protected]")), Some("b.test".to_string())); assert_eq!(domain_of(&basic("[email protected]")), Some("b.test".to_string()));
+1 -6
View File
@@ -2,8 +2,6 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::blob::UploadResponse; use crate::blob::UploadResponse;
@@ -188,10 +186,7 @@ impl ToRequestError for trc::Error {
trc::SecurityEvent::Unauthorized | trc::SecurityEvent::IpUnauthorized => { trc::SecurityEvent::Unauthorized | trc::SecurityEvent::IpUnauthorized => {
RequestError::forbidden() RequestError::forbidden()
} }
// inbuxa: legacy-protocols LP-8 is an event, never an error trc::SecurityEvent::IpBlockExpired | trc::SecurityEvent::IpAllowExpired => {
trc::SecurityEvent::IpBlockExpired
| trc::SecurityEvent::IpAllowExpired
| trc::SecurityEvent::LegacyProtocolsChanged => {
RequestError::internal_server_error() RequestError::internal_server_error()
} }
}, },
+2 -5
View File
@@ -9,9 +9,8 @@
// This file is auto-generated. Do not edit directly. // This file is auto-generated. Do not edit directly.
// inbuxa: 637 to 641 are the fork's SCIM events (SCIM-54); 642 is // inbuxa: 637 to 641 are the fork's SCIM events (SCIM-54); 642 is
// auth.legacy-protocol-refused (legacy-protocols LP-6); 643 is // auth.legacy-protocol-refused (legacy-protocols LP-6)
// security.legacy-protocols-changed (LP-8) pub const TOTAL_EVENT_COUNT: usize = 643;
pub const TOTAL_EVENT_COUNT: usize = 644;
pub const TOTAL_METRIC_COUNT: usize = 369; pub const TOTAL_METRIC_COUNT: usize = 369;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -656,8 +655,6 @@ pub enum SecurityEvent {
IpAllowExpired = 594, IpAllowExpired = 594,
IpUnauthorized = 279, IpUnauthorized = 279,
Unauthorized = 552, Unauthorized = 552,
// inbuxa: legacy-protocols LP-8
LegacyProtocolsChanged = 643,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+2 -28
View File
@@ -446,8 +446,6 @@ impl EventType {
b"security.ip-allow-expired" => EventType::Security(SecurityEvent::IpAllowExpired), b"security.ip-allow-expired" => EventType::Security(SecurityEvent::IpAllowExpired),
b"security.ip-unauthorized" => EventType::Security(SecurityEvent::IpUnauthorized), b"security.ip-unauthorized" => EventType::Security(SecurityEvent::IpUnauthorized),
b"security.unauthorized" => EventType::Security(SecurityEvent::Unauthorized), 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.startup" => EventType::Server(ServerEvent::Startup),
b"server.shutdown" => EventType::Server(ServerEvent::Shutdown), b"server.shutdown" => EventType::Server(ServerEvent::Shutdown),
b"server.startup-error" => EventType::Server(ServerEvent::StartupError), b"server.startup-error" => EventType::Server(ServerEvent::StartupError),
@@ -1213,10 +1211,6 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => "security.ip-allow-expired", EventType::Security(SecurityEvent::IpAllowExpired) => "security.ip-allow-expired",
EventType::Security(SecurityEvent::IpUnauthorized) => "security.ip-unauthorized", EventType::Security(SecurityEvent::IpUnauthorized) => "security.ip-unauthorized",
EventType::Security(SecurityEvent::Unauthorized) => "security.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::Startup) => "server.startup",
EventType::Server(ServerEvent::Shutdown) => "server.shutdown", EventType::Server(ServerEvent::Shutdown) => "server.shutdown",
EventType::Server(ServerEvent::StartupError) => "server.startup-error", EventType::Server(ServerEvent::StartupError) => "server.startup-error",
@@ -1889,8 +1883,6 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => 594, EventType::Security(SecurityEvent::IpAllowExpired) => 594,
EventType::Security(SecurityEvent::IpUnauthorized) => 279, EventType::Security(SecurityEvent::IpUnauthorized) => 279,
EventType::Security(SecurityEvent::Unauthorized) => 552, EventType::Security(SecurityEvent::Unauthorized) => 552,
// inbuxa: legacy-protocols LP-8
EventType::Security(SecurityEvent::LegacyProtocolsChanged) => 643,
EventType::Server(ServerEvent::Startup) => 393, EventType::Server(ServerEvent::Startup) => 393,
EventType::Server(ServerEvent::Shutdown) => 392, EventType::Server(ServerEvent::Shutdown) => 392,
EventType::Server(ServerEvent::StartupError) => 394, EventType::Server(ServerEvent::StartupError) => 394,
@@ -2579,8 +2571,6 @@ impl EventType {
594 => Some(EventType::Security(SecurityEvent::IpAllowExpired)), 594 => Some(EventType::Security(SecurityEvent::IpAllowExpired)),
279 => Some(EventType::Security(SecurityEvent::IpUnauthorized)), 279 => Some(EventType::Security(SecurityEvent::IpUnauthorized)),
552 => Some(EventType::Security(SecurityEvent::Unauthorized)), 552 => Some(EventType::Security(SecurityEvent::Unauthorized)),
// inbuxa: legacy-protocols LP-8
643 => Some(EventType::Security(SecurityEvent::LegacyProtocolsChanged)),
393 => Some(EventType::Server(ServerEvent::Startup)), 393 => Some(EventType::Server(ServerEvent::Startup)),
392 => Some(EventType::Server(ServerEvent::Shutdown)), 392 => Some(EventType::Server(ServerEvent::Shutdown)),
394 => Some(EventType::Server(ServerEvent::StartupError)), 394 => Some(EventType::Server(ServerEvent::StartupError)),
@@ -3000,8 +2990,6 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => Level::Info, EventType::Security(SecurityEvent::IpAllowExpired) => Level::Info,
EventType::Security(SecurityEvent::IpUnauthorized) => Level::Info, EventType::Security(SecurityEvent::IpUnauthorized) => Level::Info,
EventType::Security(SecurityEvent::Unauthorized) => 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::Startup) => Level::Info,
EventType::Server(ServerEvent::Shutdown) => Level::Info, EventType::Server(ServerEvent::Shutdown) => Level::Info,
EventType::Server(ServerEvent::Licensing) => Level::Info, EventType::Server(ServerEvent::Licensing) => Level::Info,
@@ -3210,9 +3198,7 @@ impl EventType {
EventType::Auth(AuthEvent::TooManyAttempts) => "Too many authentication attempts", EventType::Auth(AuthEvent::TooManyAttempts) => "Too many authentication attempts",
EventType::Auth(AuthEvent::ClientRegistration) => "OAuth Client registration", EventType::Auth(AuthEvent::ClientRegistration) => "OAuth Client registration",
// inbuxa: legacy-protocols LP-6 // inbuxa: legacy-protocols LP-6
EventType::Auth(AuthEvent::LegacyProtocolRefused) => { EventType::Auth(AuthEvent::LegacyProtocolRefused) => "Legacy mail protocol sign-in refused",
"Legacy mail protocol sign-in refused"
}
EventType::Auth(AuthEvent::Error) => "Authentication error", EventType::Auth(AuthEvent::Error) => "Authentication error",
EventType::Auth(AuthEvent::Warning) => "Authentication warning", EventType::Auth(AuthEvent::Warning) => "Authentication warning",
EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired", EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired",
@@ -3725,10 +3711,6 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => "IP allow expired", EventType::Security(SecurityEvent::IpAllowExpired) => "IP allow expired",
EventType::Security(SecurityEvent::IpUnauthorized) => "Unauthorized IP address", EventType::Security(SecurityEvent::IpUnauthorized) => "Unauthorized IP address",
EventType::Security(SecurityEvent::Unauthorized) => "Unauthorized access", 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::Startup) => "Starting INBUXA Server",
EventType::Server(ServerEvent::Shutdown) => "Shutting down INBUXA Server", EventType::Server(ServerEvent::Shutdown) => "Shutting down INBUXA Server",
EventType::Server(ServerEvent::StartupError) => "Server startup error", EventType::Server(ServerEvent::StartupError) => "Server startup error",
@@ -3982,9 +3964,7 @@ impl EventType {
EventType::Auth(AuthEvent::TooManyAttempts) => "Too many authentication attempts", EventType::Auth(AuthEvent::TooManyAttempts) => "Too many authentication attempts",
EventType::Auth(AuthEvent::ClientRegistration) => "Authentication error", EventType::Auth(AuthEvent::ClientRegistration) => "Authentication error",
// inbuxa: legacy-protocols LP-6 // inbuxa: legacy-protocols LP-6
EventType::Auth(AuthEvent::LegacyProtocolRefused) => { EventType::Auth(AuthEvent::LegacyProtocolRefused) => "This server allows only INBUXA webmail and JMAP apps",
"This server allows only INBUXA webmail and JMAP apps"
}
EventType::Auth(AuthEvent::Error) => "Authentication error", EventType::Auth(AuthEvent::Error) => "Authentication error",
EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired", EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired",
EventType::Imap(ImapEvent::ConnectionStart) => "IMAP error", EventType::Imap(ImapEvent::ConnectionStart) => "IMAP error",
@@ -4122,10 +4102,6 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired) => "Insufficient permissions", EventType::Security(SecurityEvent::IpAllowExpired) => "Insufficient permissions",
EventType::Security(SecurityEvent::IpUnauthorized) => "Unauthorized IP address", EventType::Security(SecurityEvent::IpUnauthorized) => "Unauthorized IP address",
EventType::Security(SecurityEvent::Unauthorized) => "Insufficient permissions", 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::ConnectionStart) => "SMTP error",
EventType::Smtp(SmtpEvent::ConnectionEnd) => "SMTP error", EventType::Smtp(SmtpEvent::ConnectionEnd) => "SMTP error",
EventType::Smtp(SmtpEvent::Error) => "SMTP error", EventType::Smtp(SmtpEvent::Error) => "SMTP error",
@@ -4687,8 +4663,6 @@ impl EventType {
EventType::Security(SecurityEvent::IpAllowExpired), EventType::Security(SecurityEvent::IpAllowExpired),
EventType::Security(SecurityEvent::IpUnauthorized), EventType::Security(SecurityEvent::IpUnauthorized),
EventType::Security(SecurityEvent::Unauthorized), EventType::Security(SecurityEvent::Unauthorized),
// inbuxa: legacy-protocols LP-8
EventType::Security(SecurityEvent::LegacyProtocolsChanged),
EventType::Server(ServerEvent::Startup), EventType::Server(ServerEvent::Startup),
EventType::Server(ServerEvent::Shutdown), EventType::Server(ServerEvent::Shutdown),
EventType::Server(ServerEvent::StartupError), EventType::Server(ServerEvent::StartupError),
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
rLRbZKj15KvcPMVmnisfCDsXXZEKks6BXpMkMpS0mlI q-OZe-InKnF24mlL56Vvt3m_IQNRybiN61MFxBSo0WY
+1 -104
View File
@@ -17,11 +17,7 @@ submission -- locked open -- is refused with the spec's words, with the right
password and with a wrong one, and refusals never add up to a disconnect password and with a wrong one, and refusals never add up to a disconnect
(LP-11). And that a normal IMAP sign-in works with the switch on, before and (LP-11). And that a normal IMAP sign-in works with the switch on, before and
after. And that while it is off, no listener the switch would close can be after. And that while it is off, no listener the switch would close can be
created, or made by an update (LP-4, test 4), and nothing advertises what is created, or made by an update (LP-4, test 4).
closed: autoconfig, autodiscover and PACC offer no IMAP, POP3 or submission,
and the suggested zone marks their SRV names not offered (LP-7, test 5).
Every change of the switch, and every refused sign-in, is an event in the
server's log (LP-8, test 14; LP-6).
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.
@@ -193,47 +189,6 @@ def smtp_auths(port, user, passwords):
return replies return replies
def advertised(admin, admin_pw):
"""What each client-configuration answer and the suggested zone offer."""
with urllib.request.urlopen(f"{HTTP}/mail/[email protected]",
timeout=30) as resp:
autoconfig = resp.read().decode()
body = ('<?xml version="1.0" encoding="utf-8"?><Autodiscover xmlns="http://schemas.'
'microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request>'
'<EMailAddress>[email protected]</EMailAddress><AcceptableResponseSchema>http://'
'schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
'</AcceptableResponseSchema></Request></Autodiscover>').encode()
req = urllib.request.Request(f"{HTTP}/autodiscover/autodiscover.xml", data=body, method="POST")
req.add_header("Content-Type", "text/xml")
with urllib.request.urlopen(req, timeout=30) as resp:
autodiscover = resp.read().decode()
with urllib.request.urlopen(f"{HTTP}/.well-known/user-agent-configuration.json",
timeout=30) as resp:
pacc = json.load(resp).get("protocols", {})
got = one(admin, admin_pw, "x:Domain/get", {"ids": None, "properties": ["name", "dnsZoneFile"]})
zone = next((d.get("dnsZoneFile") or "" for d in got[1].get("list", [])
if d.get("name") == "legacy.test"), "")
srv = {}
for line in zone.splitlines():
fields = line.split()
if "SRV" in fields and fields[0].startswith("_"):
srv[fields[0].split(".")[0] + "." + fields[0].split(".")[1]] = fields[-1]
return {
"autoconfig": {t for t in ("imap", "pop3", "smtp") if f'type="{t}"' in autoconfig},
"autodiscover": {t for t in ("IMAP", "POP3", "SMTP") if f"<Type>{t}</Type>" in autodiscover},
"pacc": {t for t in ("imap", "pop3", "smtp", "managesieve") if t in pacc},
"jmap": "jmap" in pacc,
"srv": srv,
}
def events(name):
"""The server's log lines for one event, from its stdout tracer. The log
is the container's, so it starts afresh at every restart."""
out = docker("logs", NAME, check_rc=False)
return [l for l in (out.stdout + out.stderr).splitlines() if f"({name})" in l]
def settle(port, want, tries=30): def settle(port, want, tries=30):
"""Wait for a port to reach the wanted state, so the check is not a race.""" """Wait for a port to reach the wanted state, so the check is not a race."""
for _ in range(tries): for _ in range(tries):
@@ -277,15 +232,6 @@ def main():
stop() stop()
start() start()
# A tracer to stdout, so the events can be read back from the container's
# log. It takes effect from the next start.
res = one(admin, admin_pw, "x:Tracer/set", {"create": {"t": {
"@type": "Stdout", "level": "info", "buffered": False, "ansi": False}}})
if not (res[1].get("created") or {}).get("t"):
sys.exit("tracer create failed: " + json.dumps(res))
stop()
start()
sess = session(admin, admin_pw) sess = session(admin, admin_pw)
account = sess["primaryAccounts"].get(INBUXA) or list(sess["accounts"])[0] account = sess["primaryAccounts"].get(INBUXA) or list(sess["accounts"])[0]
policy_get = {"accountId": account, "ids": None} policy_get = {"accountId": account, "ids": None}
@@ -297,15 +243,6 @@ def main():
check(accepts(PORTS["submissions"]), "submission accepts before the switch") check(accepts(PORTS["submissions"]), "submission accepts before the switch")
check(accepts(PORTS["smtp"]), "inbound SMTP accepts before the switch") check(accepts(PORTS["smtp"]), "inbound SMTP accepts before the switch")
# What is advertised with the switch on -- the control for LP-7.
before = advertised(admin, admin_pw)
print(" advertised before:", {k: sorted(v) if isinstance(v, set) else v
for k, v in before.items() if k != "srv"})
check(before["autoconfig"] and before["autodiscover"],
"autoconfig and autodiscover offer mail apps a server with the switch on")
check(before["srv"].get("_imaps._tcp", ".") != ".",
"the suggested zone offers IMAP with the switch on")
# A normal sign-in works with the switch on -- the control for LP-6. # A normal sign-in works with the switch on -- the control for LP-6.
check(imap_login(PORTS["imap"], admin, admin_pw).startswith("OK"), check(imap_login(PORTS["imap"], admin, admin_pw).startswith("OK"),
"IMAP sign-in works with the switch on") "IMAP sign-in works with the switch on")
@@ -358,19 +295,6 @@ def main():
if not all(r == SMTP_REFUSAL for r in replies): if not all(r == SMTP_REFUSAL for r in replies):
print(" replies:", replies) print(" replies:", replies)
# Nothing advertises what is closed (LP-7, test 5).
during = advertised(admin, admin_pw)
check(not during["autoconfig"], "autoconfig offers no IMAP, POP3 or submission (LP-7)")
check(not during["autodiscover"], "autodiscover offers no IMAP, POP3 or submission (LP-7)")
check(not during["pacc"] and during["jmap"], "PACC offers JMAP and nothing legacy (LP-7)")
names = ("_imap._tcp", "_imaps._tcp", "_pop3._tcp", "_pop3s._tcp",
"_submission._tcp", "_submissions._tcp")
offered = {n: t for n, t in during["srv"].items() if n in names and t != "."}
check(not offered and "_imaps._tcp" in during["srv"],
"the suggested zone marks the legacy SRV names not offered, target . (LP-7)")
if offered or "_imaps._tcp" not in during["srv"]:
print(" srv:", during["srv"])
# No listener the switch would close can be added while it is off (LP-4, # No listener the switch would close can be added while it is off (LP-4,
# test 4), and the refusal names the policy. # test 4), and the refusal names the policy.
res = one(admin, admin_pw, "x:NetworkListener/set", {"create": {"m": { res = one(admin, admin_pw, "x:NetworkListener/set", {"create": {"m": {
@@ -398,25 +322,6 @@ def main():
"turning it into an IMAP listener is refused (LP-4)") "turning it into an IMAP listener is refused (LP-4)")
one(admin, admin_pw, "x:NetworkListener/set", {"destroy": [extra]}) one(admin, admin_pw, "x:NetworkListener/set", {"destroy": [extra]})
# The change was reported (LP-8, test 14), with who made it and what closed.
changed = events("security.legacy-protocols-changed")
check(len(changed) == 1 and 'value = "disabled"' in changed[0]
and 'policy = "server"' in changed[0] and 'details = "closed"' in changed[0]
and '"imaps"' in changed[0] and "accountId = " in changed[0],
"turning it off is one event: scope, new value, who, listeners closed (LP-8)")
if len(changed) != 1:
print(" events:", changed)
# Asking for what already holds is not a change.
one(admin, admin_pw, "inbuxa:ProtocolPolicy/set", policy_set({"legacyProtocols": "disabled"}))
check(len(events("security.legacy-protocols-changed")) == 1,
"setting it off again when it is off raises no event (LP-8)")
# Every refused sign-in is an event too, and none is a failed sign-in.
refused = events("auth.legacy-protocol-refused")
check(len(refused) == 7 and all('source = "submission"' in l for l in refused),
"each refused sign-in is an auth.legacy-protocol-refused event (LP-6)")
check(not events("auth.failed") and not events("auth.too-many-attempts"),
"and none is logged as a failed sign-in (LP-11)")
# A restart must not reopen them: the objects are gone, not just the sockets. # A restart must not reopen them: the objects are gone, not just the sockets.
stop() stop()
start() start()
@@ -435,14 +340,6 @@ def main():
policy = got[1]["list"][0] policy = got[1]["list"][0]
check(policy["legacyProtocols"] == "enabled", "switch reads back enabled") check(policy["legacyProtocols"] == "enabled", "switch reads back enabled")
check(not policy["savedListeners"], "savedListeners is empty again (LP-5)") check(not policy["savedListeners"], "savedListeners is empty again (LP-5)")
changed = events("security.legacy-protocols-changed")
check(len(changed) == 1 and 'value = "enabled"' in changed[0]
and 'details = "reopened"' in changed[0] and '"imaps"' in changed[0],
"turning it back on is one event, naming the listeners reopened (LP-8)")
after = advertised(admin, admin_pw)
check(after["autoconfig"] == before["autoconfig"] and after["srv"] == before["srv"],
"autoconfig and the suggested zone offer them again once back on")
# And sign-in works again, with no restart. # And sign-in works again, with no restart.
check(imap_login(PORTS["imap"], admin, admin_pw).startswith("OK"), check(imap_login(PORTS["imap"], admin, admin_pw).startswith("OK"),