Nothing advertises the legacy protocols while they are off (LP-7)
While the switch is off, the answers that tell a mail app where to connect stop offering what the switch closed, so a new phone or desktop app is not sent to a port that is shut or a sign-in that will be refused: - Thunderbird-style autoconfig (/mail/config-v1.1.xml and its other paths) and Outlook autodiscover leave out IMAP, POP3 and SMTP submission. - PACC (/.well-known/user-agent-configuration.json) offers JMAP, CalDAV, CardDAV and WebDAV, and no IMAP, POP3, SMTP or ManageSieve. The document is rendered once per configuration load, so the JMAP-only version is rendered beside it and chosen per request; the _ua-auto-config digest in the suggested zone follows, since it hashes the same document. - The suggested zone publishes _imap, _imaps, _pop3, _pop3s, _submission and _submissions with target "." -- "not offered", RFC 6186 section 3.4 -- the spec's decision, rather than dropping them: a client that looks is told, and an automatically managed zone replaces the old records instead of leaving them behind. - It also drops the TLSA records for ports 993 and 995. A TLS pin for a port the switch has closed advertises a service that is not there. Submission's 465 keeps its record: the SMTP lock keeps that port open. The switch is read per answer, as sign-in reads it, so every node agrees the moment it turns. Inbound mail, MX records and the JMAP, CalDAV and CardDAV answers are untouched. tests/e2e/legacy_protocols.py checks all four on a running server: with the switch on they offer IMAP, POP3 and SMTP (the control); while it is off they offer none of them and every legacy SRV name has target "."; and once it is back on, autoconfig and the zone read as they did before. All checks pass.
This commit is contained in:
@@ -47,6 +47,9 @@ 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>,
|
||||||
}
|
}
|
||||||
@@ -320,11 +323,26 @@ impl Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (prefix, suffix) = serde_json::to_string(&pacc)
|
let split = |pacc: &Configuration| {
|
||||||
.unwrap_or_default()
|
serde_json::to_string(pacc)
|
||||||
.rsplit_once(SPLIT_HERE)
|
.unwrap_or_default()
|
||||||
.map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string()))
|
.rsplit_once(SPLIT_HERE)
|
||||||
.unwrap();
|
.map(|(prefix, suffix)| Pacc {
|
||||||
|
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(),
|
||||||
@@ -339,7 +357,8 @@ 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 { prefix, suffix },
|
pacc,
|
||||||
|
pacc_jmap_only,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
* 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};
|
use crate::{Server, manager::application::Resource, network::legacy::is_legacy_service};
|
||||||
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;
|
||||||
@@ -55,7 +57,12 @@ 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,9 +2,11 @@
|
|||||||
* 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};
|
use crate::{Server, manager::application::Resource, network::legacy::is_legacy_service};
|
||||||
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;
|
||||||
@@ -28,6 +30,9 @@ 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");
|
||||||
@@ -40,6 +45,9 @@ 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]),
|
||||||
|
|||||||
@@ -2,9 +2,15 @@
|
|||||||
* 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, config::network::Pacc, network::dkim::generate_dkim_dns_record};
|
use crate::{
|
||||||
|
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::{
|
||||||
@@ -33,6 +39,8 @@ 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}");
|
||||||
|
|
||||||
@@ -193,6 +201,25 @@ 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 {
|
||||||
@@ -277,6 +304,14 @@ 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,
|
||||||
@@ -382,6 +417,12 @@ 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!())
|
||||||
@@ -390,15 +431,9 @@ impl Server {
|
|||||||
.and_then(|directory| {
|
.and_then(|directory| {
|
||||||
directory
|
directory
|
||||||
.oidc_discovery_document()
|
.oidc_discovery_document()
|
||||||
.map(|doc| self.core.network.info.pacc.build(&doc.url))
|
.map(|doc| 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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
//! 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.
|
||||||
@@ -31,6 +35,7 @@ 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;
|
||||||
|
|
||||||
@@ -302,6 +307,27 @@ 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::*;
|
||||||
@@ -349,6 +375,26 @@ 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()));
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ 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).
|
created, or made by an update (LP-4, test 4), and nothing advertises what is
|
||||||
|
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).
|
||||||
|
|
||||||
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.
|
||||||
@@ -189,6 +191,40 @@ 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 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):
|
||||||
@@ -243,6 +279,15 @@ 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")
|
||||||
@@ -295,6 +340,19 @@ 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": {
|
||||||
@@ -341,6 +399,10 @@ def main():
|
|||||||
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)")
|
||||||
|
|
||||||
|
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"),
|
||||||
"IMAP sign-in works again once the switch is back on")
|
"IMAP sign-in works again once the switch is back on")
|
||||||
|
|||||||
Reference in New Issue
Block a user