Mail: admin opt-in to allow private-network IMAP/SMTP servers (like SUBSCRIPTION_ALLOWED_NETWORKS) #1160

Open
opened 2026-10-05 17:41:44 +00:00 by kayg · 9 comments
Owner

Owner request (2026-10-05, via the Mac website thread, #1143).

A self-hoster cannot add a mail account on a private network (a LAN Dovecot, a NAS, a mail server on the same Docker network). Mail resolves every IMAP and SMTP host through the public-endpoint check (crates/plugins/mail/src/imap.rs, resolve_public and validate_public_endpoint), so a private address is refused (ImapError::BlockedAddress → 400 "The provider endpoint is invalid or unavailable").

Calendar URL subscriptions already have an admin opt-in for this: CALTERNAL_SERVER__SUBSCRIPTION_ALLOWED_NETWORKS (CIDR list, empty by default, crates/plugins/calendar/src/feeds/subscriptions.rs).

Ask: add the same opt-in for Mail, for example CALTERNAL_SERVER__MAIL_ALLOWED_NETWORKS:

  • Empty by default, so public instances keep the current protection (SSRF).
  • CIDR list. A host that resolves into a listed network is accepted for IMAP and SMTP. Unspecified, broadcast, link-local metadata (169.254.169.254) and multicast addresses stay blocked even when listed, as for subscriptions.
  • TLS stays required. Self-signed certificates are not trusted by this setting.
  • Doc comment on the config field (the docs-from-code work reads it) and a regression test both ways.

Not affected: the website showcase. It uses the existing mail-test-provider feature with a local TLS fixture, so it does not wait for this issue.

Owner request (2026-10-05, via the Mac website thread, #1143). A self-hoster cannot add a mail account on a private network (a LAN Dovecot, a NAS, a mail server on the same Docker network). Mail resolves every IMAP and SMTP host through the public-endpoint check (`crates/plugins/mail/src/imap.rs`, `resolve_public` and `validate_public_endpoint`), so a private address is refused (`ImapError::BlockedAddress` → 400 "The provider endpoint is invalid or unavailable"). Calendar URL subscriptions already have an admin opt-in for this: `CALTERNAL_SERVER__SUBSCRIPTION_ALLOWED_NETWORKS` (CIDR list, empty by default, `crates/plugins/calendar/src/feeds/subscriptions.rs`). Ask: add the same opt-in for Mail, for example `CALTERNAL_SERVER__MAIL_ALLOWED_NETWORKS`: - Empty by default, so public instances keep the current protection (SSRF). - CIDR list. A host that resolves into a listed network is accepted for IMAP and SMTP. Unspecified, broadcast, link-local metadata (169.254.169.254) and multicast addresses stay blocked even when listed, as for subscriptions. - TLS stays required. Self-signed certificates are not trusted by this setting. - Doc comment on the config field (the docs-from-code work reads it) and a regression test both ways. Not affected: the website showcase. It uses the existing `mail-test-provider` feature with a local TLS fixture, so it does not wait for this issue.
Author
Owner

Starting mailnet-1160 on branch job/mailnet-1160. Initial base SHA: a493c314ed; origin/dev was at d57992dea7 and this worktree is six commits behind. I have read CLAUDE.md, CONTEXT.md, docs/DESIGN.md §45/§49, and issue #1160. I am tracing the shared Calendar network policy and Mail connect-time resolution before editing.

Starting mailnet-1160 on branch job/mailnet-1160. Initial base SHA: a493c314edfd40027a72e0d7fc1cb97fa69b1eb9; origin/dev was at d57992dea77a5c031b1494fe9e5b608505b707e8 and this worktree is six commits behind. I have read CLAUDE.md, CONTEXT.md, docs/DESIGN.md §45/§49, and issue #1160. I am tracing the shared Calendar network policy and Mail connect-time resolution before editing.
Author
Owner

Finding: Calendar’s old is_fetchable_with checked only unspecified, broadcast, and multicast after CIDR matching. A listed 169.254.0.0/16 or fe80::/10 therefore admitted link-local targets, including cloud metadata. I replaced that local check with the shared policy and added tests that exercise metadata and both link-local families under broad CIDRs.

Finding: Calendar’s old `is_fetchable_with` checked only unspecified, broadcast, and multicast after CIDR matching. A listed `169.254.0.0/16` or `fe80::/10` therefore admitted link-local targets, including cloud metadata. I replaced that local check with the shared policy and added tests that exercise metadata and both link-local families under broad CIDRs.
Author
Owner

Verification finding: the server crate run reported 255 passed, 10 ignored, and one failure in wire::tests::live_apps_run_in_separate_processes. Its child startup_search_reconciliation_keeps_calendar_writes_responsive_on_large_home measured 20,000 files and 32 writes at p95 4,143 ms against a 10,000 ms budget, then failed because Search reconciliation did not finish. Another worktree was compiling on the shared host. I left the unrelated startup path unchanged and classify this as a SLOW host-load result under the owner rule.

After the Oct 6 origin/dev sync, the exact perf ledger check passes with 22,103 scoped entries and no ratchet growth. The updated web suite also passes; final Mail adversarial verification is still in progress.

Verification finding: the server crate run reported 255 passed, 10 ignored, and one failure in `wire::tests::live_apps_run_in_separate_processes`. Its child `startup_search_reconciliation_keeps_calendar_writes_responsive_on_large_home` measured 20,000 files and 32 writes at p95 4,143 ms against a 10,000 ms budget, then failed because Search reconciliation did not finish. Another worktree was compiling on the shared host. I left the unrelated startup path unchanged and classify this as a SLOW host-load result under the owner rule. After the Oct 6 `origin/dev` sync, the exact perf ledger check passes with 22,103 scoped entries and no ratchet growth. The updated web suite also passes; final Mail adversarial verification is still in progress.
Author
Owner

The first real-server Mail network-policy probe found that bare IPv6 host literals (for example fe80::1) were rejected by route syntax validation before the shared network policy ran. The existing IMAP normalizer expects URL-style brackets, so the probe received “The provider host is invalid” instead of the endpoint-policy result. I am fixing the shared Mail host normalization and adding a regression; the address policy itself remains fail-closed.

The first real-server Mail network-policy probe found that bare IPv6 host literals (for example `fe80::1`) were rejected by route syntax validation before the shared network policy ran. The existing IMAP normalizer expects URL-style brackets, so the probe received “The provider host is invalid” instead of the endpoint-policy result. I am fixing the shared Mail host normalization and adding a regression; the address policy itself remains fail-closed.
Author
Owner

Implemented Forgejo #1160. Final head: fd095d0f8ccf66c72fccfee86fadc458733d6115.

Built

  • Added a shared OutboundNetworkPolicy for Calendar subscriptions and Mail. It parses comma-separated CIDRs, matches IPv4-mapped IPv6 as IPv4, allows operator-listed private destinations, and always rejects unspecified, broadcast, link-local and multicast destinations.
  • Added empty-by-default CALTERNAL_SERVER__MAIL_ALLOWED_NETWORKS startup configuration and its STE field documentation. An invalid CIDR stops startup.
  • Applied the shared policy to Mail IMAP and SMTP endpoint checks. IMAP resolves again for each connect or reconnect and pins the checked socket address. The allowlist does not add TLS roots.
  • Added a real-server adversarial probe and registered it in tests/adversarial/run.sh.
  • Added a typical and worst-case policy profile. On the locked perf VM it measured average 4 CIDRs/4 answers/32 repeats at p50 632 ns and p95 1,130 ns; worst case 256 CIDRs/32 answers/8 repeats at p50 479,911 ns and p95 535,417 ns. CPU was 0.79 user seconds, peak RSS 3,468 KiB. docs/perf/baseline.json has no comparable resolver workload.
  • Fixed bare IPv6 provider inputs found by the real-server probe; route validation and connection normalization now use the same host parser.

Files

Shared policy and resolver: crates/calternal-api/src/public_address.rs, crates/calternal-plugin/src/outbound.rs, workspace and crate Cargo manifests, Cargo.lock.

Settings and consumers: crates/calternal-server/src/main.rs, crates/calternal-server/src/wire.rs, crates/plugins/calendar/src/feeds/subscriptions.rs, crates/plugins/calendar/src/lib.rs, crates/plugins/mail/src/imap.rs, crates/plugins/mail/src/lib.rs, crates/plugins/mail/src/routes.rs.

Probe and performance: tests/adversarial/mail_network_policy.mjs, tests/adversarial/run.sh, bench/mail-network-policy.sh, contracts/perf/exceptions.json, contracts/perf/ratchet.json.

Verification

cargo fmt --check: exit 0, no stdout.

Per-crate clippy passed for calternal-api, calternal-plugin, calternal-plugin-calendar, calternal-plugin-mail, and calternal-server. Their tests passed for API (20 passed, 1 ignored), Plugin (44 passed), Calendar (100 passed, 1 ignored), and Mail (125 passed, 6 ignored), except for the one SLOW server test below.

Final Mail gate output excerpts:

Checking calternal-api v0.0.1 (/home/kayg/Developer/calternal-wt/mailnet-1160/crates/calternal-api)
Checking calternal-plugin v0.0.1 (/home/kayg/Developer/calternal-wt/mailnet-1160/crates/calternal-plugin)
Checking calternal-plugin-mail v0.0.1 (/home/kayg/Developer/calternal-wt/mailnet-1160/crates/plugins/mail)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 19.21s

 test result: ok. 125 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out; finished in 37.57s

The current server build passed. The server crate test run reported 255 passed, 10 ignored, and one SLOW failure in wire::tests::live_apps_run_in_separate_processes; its child startup_search_reconciliation_keeps_calendar_writes_responsive_on_large_home did not finish with 20,000 files under concurrent host load. Its 32-write p95 was 4,143 ms against a 10,000 ms budget. I posted this finding earlier; it is load-only under the owner rule.

After merging current origin/dev, web checks passed:

svelte-check found 0 errors and 4 warnings in 3 files
Test Files 266 passed (266)
Tests 1827 passed (1827)

perf-lint --check passed with 0 violations; 22103 scoped exceptions. The focused real-server probe passed:

Mail network-policy probe: IMAP and SMTP reject unlisted private, metadata, link-local, multicast, unspecified, broadcast, and mapped IPv6 targets even when those ranges are listed; listed private access keeps TLS verification enabled

The policy profile passed with the measurements above. node --check, bash -n, and git diff --check passed. cargo clean removed 22,788 files (18.6 GiB), and the web build output was deleted.

UX gaps

No UI changed. UX gaps closed or left: none in scope.

Known gaps

The server package has the single SLOW test result above. The full adversarial matrix and full workspace gates remain for the merge round.

Decisions not specified by DESIGN

  • An empty Mail allowlist preserves public-only access. Explicitly listed ordinary private and loopback unicast destinations are allowed; unspecified, broadcast, link-local and multicast addresses remain blocked even when listed.
  • Bare and bracketed IPv6 literals are both accepted as host input. The resolver brackets IPv6 before lookup and checks mapped IPv6 using its IPv4 value.

No push, deploy, merge, or issue close was performed.

Implemented Forgejo #1160. Final head: `fd095d0f8ccf66c72fccfee86fadc458733d6115`. ## Built - Added a shared `OutboundNetworkPolicy` for Calendar subscriptions and Mail. It parses comma-separated CIDRs, matches IPv4-mapped IPv6 as IPv4, allows operator-listed private destinations, and always rejects unspecified, broadcast, link-local and multicast destinations. - Added empty-by-default `CALTERNAL_SERVER__MAIL_ALLOWED_NETWORKS` startup configuration and its STE field documentation. An invalid CIDR stops startup. - Applied the shared policy to Mail IMAP and SMTP endpoint checks. IMAP resolves again for each connect or reconnect and pins the checked socket address. The allowlist does not add TLS roots. - Added a real-server adversarial probe and registered it in `tests/adversarial/run.sh`. - Added a typical and worst-case policy profile. On the locked perf VM it measured average 4 CIDRs/4 answers/32 repeats at p50 632 ns and p95 1,130 ns; worst case 256 CIDRs/32 answers/8 repeats at p50 479,911 ns and p95 535,417 ns. CPU was 0.79 user seconds, peak RSS 3,468 KiB. `docs/perf/baseline.json` has no comparable resolver workload. - Fixed bare IPv6 provider inputs found by the real-server probe; route validation and connection normalization now use the same host parser. ## Files Shared policy and resolver: `crates/calternal-api/src/public_address.rs`, `crates/calternal-plugin/src/outbound.rs`, workspace and crate Cargo manifests, `Cargo.lock`. Settings and consumers: `crates/calternal-server/src/main.rs`, `crates/calternal-server/src/wire.rs`, `crates/plugins/calendar/src/feeds/subscriptions.rs`, `crates/plugins/calendar/src/lib.rs`, `crates/plugins/mail/src/imap.rs`, `crates/plugins/mail/src/lib.rs`, `crates/plugins/mail/src/routes.rs`. Probe and performance: `tests/adversarial/mail_network_policy.mjs`, `tests/adversarial/run.sh`, `bench/mail-network-policy.sh`, `contracts/perf/exceptions.json`, `contracts/perf/ratchet.json`. ## Verification `cargo fmt --check`: exit 0, no stdout. Per-crate clippy passed for `calternal-api`, `calternal-plugin`, `calternal-plugin-calendar`, `calternal-plugin-mail`, and `calternal-server`. Their tests passed for API (20 passed, 1 ignored), Plugin (44 passed), Calendar (100 passed, 1 ignored), and Mail (125 passed, 6 ignored), except for the one SLOW server test below. Final Mail gate output excerpts: ```text Checking calternal-api v0.0.1 (/home/kayg/Developer/calternal-wt/mailnet-1160/crates/calternal-api) Checking calternal-plugin v0.0.1 (/home/kayg/Developer/calternal-wt/mailnet-1160/crates/calternal-plugin) Checking calternal-plugin-mail v0.0.1 (/home/kayg/Developer/calternal-wt/mailnet-1160/crates/plugins/mail) Finished `dev` profile [unoptimized + debuginfo] target(s) in 19.21s test result: ok. 125 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out; finished in 37.57s ``` The current server build passed. The server crate test run reported 255 passed, 10 ignored, and one SLOW failure in `wire::tests::live_apps_run_in_separate_processes`; its child `startup_search_reconciliation_keeps_calendar_writes_responsive_on_large_home` did not finish with 20,000 files under concurrent host load. Its 32-write p95 was 4,143 ms against a 10,000 ms budget. I posted this finding earlier; it is load-only under the owner rule. After merging current `origin/dev`, web checks passed: ```text svelte-check found 0 errors and 4 warnings in 3 files Test Files 266 passed (266) Tests 1827 passed (1827) ``` `perf-lint --check` passed with `0 violations; 22103 scoped exceptions`. The focused real-server probe passed: ```text Mail network-policy probe: IMAP and SMTP reject unlisted private, metadata, link-local, multicast, unspecified, broadcast, and mapped IPv6 targets even when those ranges are listed; listed private access keeps TLS verification enabled ``` The policy profile passed with the measurements above. `node --check`, `bash -n`, and `git diff --check` passed. `cargo clean` removed 22,788 files (18.6 GiB), and the web build output was deleted. ## UX gaps No UI changed. UX gaps closed or left: none in scope. ## Known gaps The server package has the single SLOW test result above. The full adversarial matrix and full workspace gates remain for the merge round. ## Decisions not specified by DESIGN - An empty Mail allowlist preserves public-only access. Explicitly listed ordinary private and loopback unicast destinations are allowed; unspecified, broadcast, link-local and multicast addresses remain blocked even when listed. - Bare and bracketed IPv6 literals are both accepted as host input. The resolver brackets IPv6 before lookup and checks mapped IPv6 using its IPv4 value. No push, deploy, merge, or issue close was performed.
Author
Owner

Independent review started on job/mailnet-1160 at fd095d0f8c; base 41bf97fb28. Read the issue and implementation diff before author comments. Initial finding: broad CIDRs currently admit loopback and IPv6 transition destinations. I will add regression tests and fix this boundary. No push or deployment.

Independent review started on job/mailnet-1160 at fd095d0f8ccf66c72fccfee86fadc458733d6115; base 41bf97fb2861ea6e4ea70aab7d3b15a1c9aa278d. Read the issue and implementation diff before author comments. Initial finding: broad CIDRs currently admit loopback and IPv6 transition destinations. I will add regression tests and fix this boundary. No push or deployment.
Author
Owner

Confirmed a security boundary defect at author head fd095d0f8: the new review regression failed with 127.0.0.1 under 0.0.0.0/0,::/0. Broad CIDRs also admitted IPv4-compatible, NAT64 and 6to4 forms. Fixed in 65f7bd420 with permanent exclusions before CIDR matching, as required by the review brief. The empty-list policy still matches the original public classifier. API clippy passed; API tests: test result: ok. 22 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.09s.

Decision: the review brief's stricter rule rejects private mapped IPv6 even if its IPv4 network is listed; public mapped IPv6 retains existing behavior. Existing private-mapped assertions will change only for this explicitly requested rule. Calendar refresh response/body assertions remain unchanged; local fixtures move from loopback to a real host interface. SMTP submission does not exist (DESIGN §53), so future submission cannot be certified here.

Confirmed a security boundary defect at author head fd095d0f8: the new review regression failed with `127.0.0.1 under 0.0.0.0/0,::/0`. Broad CIDRs also admitted IPv4-compatible, NAT64 and 6to4 forms. Fixed in 65f7bd420 with permanent exclusions before CIDR matching, as required by the review brief. The empty-list policy still matches the original public classifier. API clippy passed; API tests: `test result: ok. 22 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.09s`. Decision: the review brief's stricter rule rejects private mapped IPv6 even if its IPv4 network is listed; public mapped IPv6 retains existing behavior. Existing private-mapped assertions will change only for this explicitly requested rule. Calendar refresh response/body assertions remain unchanged; local fixtures move from loopback to a real host interface. SMTP submission does not exist (DESIGN §53), so future submission cannot be certified here.
Author
Owner

Second finding: Rust main prints a returned error through Debug. The original derived output was only NetworkPolicyParseError, which hid its invalid-CIDR explanation. Commit bf48028af makes that diagnostic clear without printing the setting value, and extends the permanent transition exclusions. Latest API clippy passed. The updated policy module's focused tests pass: test result: ok. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s; the full crate test remains queued behind the Calendar dependency build.

Web tests after the origin/dev merge passed: Test Files 266 passed (266); Tests 1830 passed (1830). Exact performance pins have been refreshed for the two networking modules; their live total is 22104, equal to origin/dev's ceiling. No rule or threshold is weakened. Full Rust gates and the focused real-server probe remain in progress.

Second finding: Rust main prints a returned error through Debug. The original derived output was only `NetworkPolicyParseError`, which hid its invalid-CIDR explanation. Commit bf48028af makes that diagnostic clear without printing the setting value, and extends the permanent transition exclusions. Latest API clippy passed. The updated policy module's focused tests pass: `test result: ok. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s`; the full crate test remains queued behind the Calendar dependency build. Web tests after the origin/dev merge passed: `Test Files 266 passed (266)`; `Tests 1830 passed (1830)`. Exact performance pins have been refreshed for the two networking modules; their live total is 22104, equal to origin/dev's ceiling. No rule or threshold is weakened. Full Rust gates and the focused real-server probe remain in progress.
Author
Owner

Independent defensive review: #1160

SAFE TO MERGE: YES for the network change.

Final head: 51b8aabffe3f9ffdc59fc19dbc2b45435d93095a. Branch: job/mailnet-1160. No push or deploy.

Independent defensive review of #1160

Reviewed author head fd095d0f8c against origin/dev (merge base 41bf97fb28). Read the issue and diff before author reports.

Findings

  1. The shared policy only permanently blocks unspecified, broadcast, link-local and multicast. 0.0.0.0/0 therefore opens loopback; ::/0 opens IPv4-compatible, NAT64 and 6to4 destinations. Mapped private IPv6 is accepted after normalization. Added review_1160_never_allows_local_or_transition_destinations for the review brief's stricter invariant.
  2. Empty policy uses the unchanged public classifier. Added a representative IPv4/IPv6 equivalence test for Mail and Calendar's shared policy.
  3. IMAP resolves afresh in connect, connects to a SocketAddr, and passes the original normalized ServerName to rustls. No DNS lookup occurs in TLS. Sync, IDLE reconnect and queued delivery call this same entry. Calendar validates all DNS answers and passes them to reqwest resolve_to_addrs; redirects resolve again. Mixed answers fail closed.
  4. SMTP submission does not exist (DESIGN §53). SMTP settings use the same address check, but no outbound SMTP connection or SMTP TLS handshake exists to test. This review cannot certify future submission behavior.
  5. Calendar's existing conditional-refresh test fixture explicitly lists loopback. The review's prohibition requires a fixture on the host's real interface; retain all refresh status/body expectations.
  6. The original adversarial probe likewise treats loopback as listed private and expects a TLS handshake. Its fixture must move to a real interface; retain self-signed rejection evidence.

Decisions

The review brief explicitly requires loopback and transition forms to stay blocked even under broad CIDRs. Apply that stricter requirement to the shared policy. Preserve public mapped IPv6 and the empty-policy classifier. Ordinary listed RFC1918 and IPv6 ULA destinations remain allowed.

Verification

Pending.

Confirmed before the fix: the new focused test failed with 127.0.0.1 under 0.0.0.0/0,::/0; the empty-policy equivalence test passed. Commit 65f7bd420 permanently blocks loopback, private mapped IPv6, IPv4-compatible IPv6, standard NAT64 prefixes, 6to4 and reserved IPv4 0/8 and 240/4 before CIDR matching. API clippy passed; API tests: test result: ok. 22 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.09s.

Merged origin/dev once before final gates. No config.json docs contract exists in the fetched dev tree yet; config field comments are updated in source. No new package or crate dependency is required.

  1. Startup returns a boxed error from Rust main, which prints Debug rather than Display. The derived Debug output was only NetworkPolicyParseError. Added a safe Debug implementation that prints the invalid-CIDR explanation without the raw setting. Added independent malformed-prefix and partly valid-list cases, plus real startup probes for both settings.

  2. Added production-path tests for checked-socket authentication. A hostname under .invalid still reaches the supplied local socket, so authentication cannot perform a second DNS lookup. Captured the actual TLS ClientHello SNI and checked that it retains that hostname. Missing STARTTLS and explicit STARTTLS refusal end before LOGIN. The shared Mail transport type rejects plaintext modes.

  3. Extracted the existing complete-answer validation loop for deterministic tests: both mixed public/private answer orders fail; listed private answers pass; changed reconnect answers fail; empty and oversized answer sets fail. This preserves the existing production checks and returned SocketAddr values.

  4. The empty policy still uses the original public classifier. Standard transition-prefix exclusions include NAT64, 6to4, Teredo and translated/compatible forms. The relevant standard prefixes were checked against the IANA registry and RFC 6145. Arbitrary administrator-defined translation prefixes cannot be identified from an IPv6 address alone.

  5. Web check found stale exact performance pins. The author normalizer still had pins for its older Host::parse body. Refresh only the live sites in the two touched networking modules. Use a direct match for bare IPv6 parsing and let ServerName reject an empty domain, as it already must. This retains host behavior and avoids adding unresolved-call debt above dev. Keep tests for empty and dot-only hosts.

Web gates passed after the origin/dev merge and exact pin refresh. perf-lint: PASS; 0 violations; 22104 scoped exceptions. svelte-check found 0 errors and 4 warnings in 3 files. Full web tests: Test Files 266 passed (266) and Tests 1830 passed (1830). The production web build also passed. Calendar clippy passed. The cold test dependency build still holds the Cargo lock; full API and other crate test results are pending. Latest API clippy and the fresh focused policy module tests passed before commit bf48028af.

Final verdict

SAFE TO MERGE: yes for #1160. No non-SLOW network finding remains.

Code head: 62838495ad. The final report commit follows this code head.

The initial full Server suite failed one 20-second Calendar publication-write timeout during startup Search reconciliation. This route does not use the outbound address policy. The same case passed in isolation with the wrapper's required 4 MiB stack. The two live-app cases skipped after that timeout and both integration targets passed separately. Keep the initial failure in the record. The first isolated invocation omitted RUST_MIN_STACK and aborted; the corrected invocation passed. No test expectation was weakened.

Built and files

Permanent loopback and transition exclusions, safe startup error diagnostics, complete-answer and reconnect tests, checked-socket TLS/SNI and STARTTLS tests, real-interface Calendar/TLS fixtures, malformed-config startup probes, and exact performance pins.

crates/calternal-api/src/public_address.rs; crates/calternal-plugin/src/outbound.rs; crates/plugins/mail/src/imap.rs; crates/plugins/calendar/src/feeds/subscriptions.rs; crates/calternal-server/src/main.rs; tests/adversarial/mail_network_policy.mjs; contracts/perf/exceptions.json; contracts/perf/ratchet.json; review-1160.md.

Decisions

The review brief decides the stricter rule: private mapped IPv6 cannot use CIDR exceptions. Public mapped IPv6 keeps its prior behavior. Listed canonical RFC1918 and ULA addresses still work. Test publishers use the real host interface. The exact performance ceiling equals origin/dev (22104 entries). No config.json contract exists in the fetched dev tree; the source field comments state the comma-separated format and TLS rule.

Known gaps

SMTP submission does not exist (DESIGN §53); SMTP settings use the same checked resolver. No future SMTP implementation is certified here. TTL-zero safety is proved with deterministic answer changes and a real checked socket whose TLS hostname cannot resolve; this job did not run a separate DNS rebinding service. Only known standard transition prefixes can be identified from an address; arbitrary operator-defined NAT64 prefixes need routing context. The full Server gate retains the SLOW failure above. Web check has four existing warnings and no errors. UI is unchanged; UX gaps closed/left and screenshots do not apply. No performance profile was run because this is a defensive review, not a performance issue.

Gate excerpts (verbatim)

cargo fmt --check exited 0 with no output. Each Rust command used line-tables-only debug, no incremental build, four build jobs and the worktree TMPDIR.

cargo clippy -p calternal-api --all-targets -- -D warnings:

Finished `dev` profile [unoptimized + debuginfo] target(s) in 13m 15s

cargo clippy -p calternal-plugin --all-targets -- -D warnings:

Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 14s

cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings:

Finished `dev` profile [unoptimized + debuginfo] target(s) in 19m 15s

cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings:

Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 44s

cargo clippy -p calternal-server --all-targets -- -D warnings:

Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 05s

cargo test -p calternal-api:

test result: ok. 23 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.12s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo test -p calternal-plugin:

test result: ok. 45 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.87s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo test -p calternal-plugin-calendar:

test result: ok. 100 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 29.30s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo test -p calternal-plugin-mail:

test result: ok. 128 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out; finished in 59.32s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo test -p calternal-server:

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 265 filtered out; finished in 48.03s
test result: FAILED. 255 passed; 1 failed; 10 ignored; 0 measured; 0 filtered out; finished in 110.52s

server-focused-stack-correct:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 265 filtered out; finished in 99.83s

server-remaining-setup:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 265 filtered out; finished in 32.69s

server-remaining-freshness:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 265 filtered out; finished in 26.20s

server-integration:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 50.21s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.44s

bun run check and bun run test --maxWorkers=2:

perf-lint: PASS; 0 violations; 22104 scoped exceptions
svelte-check found 0 errors and 4 warnings in 3 files
 Test Files  266 passed (266)
      Tests  1830 passed (1830)

Focused local probe:

Mail network-policy probe: IMAP and SMTP reject unlisted private, loopback, metadata, link-local, multicast, unspecified, broadcast, mapped/compatible IPv6, NAT64 and 6to4 targets even when those ranges are listed; malformed Mail and Calendar CIDRs fail startup; listed private access keeps TLS verification enabled

For the merge round

cargo test -p calternal-server -- --test-threads=4: confirm the combined branch passes the full suite and the large-Home Calendar responsiveness case. tests/adversarial/run.sh: run the full real-server authorization and robustness matrices once on the combined branch. No full matrix, release build, staging deploy or Mac interop was run in this review.

# Independent defensive review: #1160 SAFE TO MERGE: YES for the network change. Final head: `51b8aabffe3f9ffdc59fc19dbc2b45435d93095a`. Branch: `job/mailnet-1160`. No push or deploy. # Independent defensive review of #1160 Reviewed author head fd095d0f8ccf66c72fccfee86fadc458733d6115 against origin/dev (merge base 41bf97fb2861ea6e4ea70aab7d3b15a1c9aa278d). Read the issue and diff before author reports. ## Findings 1. The shared policy only permanently blocks unspecified, broadcast, link-local and multicast. `0.0.0.0/0` therefore opens loopback; `::/0` opens IPv4-compatible, NAT64 and 6to4 destinations. Mapped private IPv6 is accepted after normalization. Added `review_1160_never_allows_local_or_transition_destinations` for the review brief's stricter invariant. 2. Empty policy uses the unchanged public classifier. Added a representative IPv4/IPv6 equivalence test for Mail and Calendar's shared policy. 3. IMAP resolves afresh in `connect`, connects to a `SocketAddr`, and passes the original normalized `ServerName` to rustls. No DNS lookup occurs in TLS. Sync, IDLE reconnect and queued delivery call this same entry. Calendar validates all DNS answers and passes them to reqwest `resolve_to_addrs`; redirects resolve again. Mixed answers fail closed. 4. SMTP submission does not exist (DESIGN §53). SMTP settings use the same address check, but no outbound SMTP connection or SMTP TLS handshake exists to test. This review cannot certify future submission behavior. 5. Calendar's existing conditional-refresh test fixture explicitly lists loopback. The review's prohibition requires a fixture on the host's real interface; retain all refresh status/body expectations. 6. The original adversarial probe likewise treats loopback as listed private and expects a TLS handshake. Its fixture must move to a real interface; retain self-signed rejection evidence. ## Decisions The review brief explicitly requires loopback and transition forms to stay blocked even under broad CIDRs. Apply that stricter requirement to the shared policy. Preserve public mapped IPv6 and the empty-policy classifier. Ordinary listed RFC1918 and IPv6 ULA destinations remain allowed. ## Verification Pending. Confirmed before the fix: the new focused test failed with `127.0.0.1 under 0.0.0.0/0,::/0`; the empty-policy equivalence test passed. Commit 65f7bd420 permanently blocks loopback, private mapped IPv6, IPv4-compatible IPv6, standard NAT64 prefixes, 6to4 and reserved IPv4 0/8 and 240/4 before CIDR matching. API clippy passed; API tests: `test result: ok. 22 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.09s`. Merged origin/dev once before final gates. No config.json docs contract exists in the fetched dev tree yet; config field comments are updated in source. No new package or crate dependency is required. 7. Startup returns a boxed error from Rust main, which prints Debug rather than Display. The derived Debug output was only `NetworkPolicyParseError`. Added a safe Debug implementation that prints the invalid-CIDR explanation without the raw setting. Added independent malformed-prefix and partly valid-list cases, plus real startup probes for both settings. 8. Added production-path tests for checked-socket authentication. A hostname under `.invalid` still reaches the supplied local socket, so authentication cannot perform a second DNS lookup. Captured the actual TLS ClientHello SNI and checked that it retains that hostname. Missing STARTTLS and explicit STARTTLS refusal end before LOGIN. The shared Mail transport type rejects plaintext modes. 9. Extracted the existing complete-answer validation loop for deterministic tests: both mixed public/private answer orders fail; listed private answers pass; changed reconnect answers fail; empty and oversized answer sets fail. This preserves the existing production checks and returned SocketAddr values. 10. The empty policy still uses the original public classifier. Standard transition-prefix exclusions include NAT64, 6to4, Teredo and translated/compatible forms. The relevant standard prefixes were checked against the [IANA registry](https://www.iana.org/assignments/iana-ipv6-special-registry) and [RFC 6145](https://www.rfc-editor.org/rfc/rfc6145.html). Arbitrary administrator-defined translation prefixes cannot be identified from an IPv6 address alone. 11. Web check found stale exact performance pins. The author normalizer still had pins for its older `Host::parse` body. Refresh only the live sites in the two touched networking modules. Use a direct match for bare IPv6 parsing and let ServerName reject an empty domain, as it already must. This retains host behavior and avoids adding unresolved-call debt above dev. Keep tests for empty and dot-only hosts. Web gates passed after the origin/dev merge and exact pin refresh. `perf-lint: PASS; 0 violations; 22104 scoped exceptions`. `svelte-check found 0 errors and 4 warnings in 3 files`. Full web tests: `Test Files 266 passed (266)` and `Tests 1830 passed (1830)`. The production web build also passed. Calendar clippy passed. The cold test dependency build still holds the Cargo lock; full API and other crate test results are pending. Latest API clippy and the fresh focused policy module tests passed before commit bf48028af. ## Final verdict SAFE TO MERGE: yes for #1160. No non-SLOW network finding remains. Code head: 62838495ada140834fcc5423a9129b2adaa582f4. The final report commit follows this code head. The initial full Server suite failed one 20-second Calendar publication-write timeout during startup Search reconciliation. This route does not use the outbound address policy. The same case passed in isolation with the wrapper's required 4 MiB stack. The two live-app cases skipped after that timeout and both integration targets passed separately. Keep the initial failure in the record. The first isolated invocation omitted RUST_MIN_STACK and aborted; the corrected invocation passed. No test expectation was weakened. ### Built and files Permanent loopback and transition exclusions, safe startup error diagnostics, complete-answer and reconnect tests, checked-socket TLS/SNI and STARTTLS tests, real-interface Calendar/TLS fixtures, malformed-config startup probes, and exact performance pins. `crates/calternal-api/src/public_address.rs`; `crates/calternal-plugin/src/outbound.rs`; `crates/plugins/mail/src/imap.rs`; `crates/plugins/calendar/src/feeds/subscriptions.rs`; `crates/calternal-server/src/main.rs`; `tests/adversarial/mail_network_policy.mjs`; `contracts/perf/exceptions.json`; `contracts/perf/ratchet.json`; `review-1160.md`. ### Decisions The review brief decides the stricter rule: private mapped IPv6 cannot use CIDR exceptions. Public mapped IPv6 keeps its prior behavior. Listed canonical RFC1918 and ULA addresses still work. Test publishers use the real host interface. The exact performance ceiling equals origin/dev (22104 entries). No config.json contract exists in the fetched dev tree; the source field comments state the comma-separated format and TLS rule. ### Known gaps SMTP submission does not exist (DESIGN §53); SMTP settings use the same checked resolver. No future SMTP implementation is certified here. TTL-zero safety is proved with deterministic answer changes and a real checked socket whose TLS hostname cannot resolve; this job did not run a separate DNS rebinding service. Only known standard transition prefixes can be identified from an address; arbitrary operator-defined NAT64 prefixes need routing context. The full Server gate retains the SLOW failure above. Web check has four existing warnings and no errors. UI is unchanged; UX gaps closed/left and screenshots do not apply. No performance profile was run because this is a defensive review, not a performance issue. ### Gate excerpts (verbatim) `cargo fmt --check` exited 0 with no output. Each Rust command used line-tables-only debug, no incremental build, four build jobs and the worktree TMPDIR. `cargo clippy -p calternal-api --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 13m 15s ``` `cargo clippy -p calternal-plugin --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 14s ``` `cargo clippy -p calternal-plugin-calendar --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 19m 15s ``` `cargo clippy -p calternal-plugin-mail --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 44s ``` `cargo clippy -p calternal-server --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 05s ``` `cargo test -p calternal-api`: ```text test result: ok. 23 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.12s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo test -p calternal-plugin`: ```text test result: ok. 45 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.87s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo test -p calternal-plugin-calendar`: ```text test result: ok. 100 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 29.30s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo test -p calternal-plugin-mail`: ```text test result: ok. 128 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out; finished in 59.32s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo test -p calternal-server`: ```text test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 265 filtered out; finished in 48.03s test result: FAILED. 255 passed; 1 failed; 10 ignored; 0 measured; 0 filtered out; finished in 110.52s ``` server-focused-stack-correct: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 265 filtered out; finished in 99.83s ``` server-remaining-setup: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 265 filtered out; finished in 32.69s ``` server-remaining-freshness: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 265 filtered out; finished in 26.20s ``` server-integration: ```text test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 50.21s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.44s ``` `bun run check` and `bun run test --maxWorkers=2`: ```text perf-lint: PASS; 0 violations; 22104 scoped exceptions svelte-check found 0 errors and 4 warnings in 3 files Test Files 266 passed (266) Tests 1830 passed (1830) ``` Focused local probe: ```text Mail network-policy probe: IMAP and SMTP reject unlisted private, loopback, metadata, link-local, multicast, unspecified, broadcast, mapped/compatible IPv6, NAT64 and 6to4 targets even when those ranges are listed; malformed Mail and Calendar CIDRs fail startup; listed private access keeps TLS verification enabled ``` ### For the merge round `cargo test -p calternal-server -- --test-threads=4`: confirm the combined branch passes the full suite and the large-Home Calendar responsiveness case. `tests/adversarial/run.sh`: run the full real-server authorization and robustness matrices once on the combined branch. No full matrix, release build, staging deploy or Mac interop was run in this review.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#1160
No description provided.