Commit Graph
409 Commits
Author SHA1 Message Date
jcoffey-dev afffa0fc96 Merge pull request 'Try a directory before anything signs in through it' (#95) from feature/directory-test into main
ci / fork-checks (push) Canceled after 21s
ci / build (push) Canceled after 21s
2026-09-28 21:51:56 +00:00
jcoffey-dev 32b22d0828 Merge pull request 'Schema: each expression field says which values and variables it accepts' (#91) from feature/expression-schema into main
ci / fork-checks (push) Canceled after 20s
ci / build (push) Canceled after 20s
2026-09-28 21:51:34 +00:00
jcoffey-dev 558b776e9f Merge pull request 'Release 2026.9.28.4' (#94) from release/2026.9.28.4-pr into main
ci / fork-checks (push) Successful in 15s
publish / version (push) Successful in 2m18s
publish / publish-amd64 (push) Successful in 29m39s
publish / release (push) Successful in 1s
ci / build (push) Successful in 59m47s
publish / publish-arm64 (push) Successful in 44m48s
publish / binaries (push) Successful in 45s
publish / announce (push) Successful in 23s
v2026.9.28.4
2026-09-28 19:46:36 +00:00
jcoffey-dev ac3a63973d Try a directory before anything signs in through it
ci / fork-checks (pull_request) Successful in 59s
ci / build (pull_request) Successful in 4m5s
POST /api/directory/test takes a saved directory's id, an address and
optionally a password, and answers whether the directory opened, what a
recipient lookup of the address finds (account or group, with its
aliases, groups and name), and whether the password signs in. A wrong
password is told apart from a directory that can't be reached or is set
up wrong.

It calls the directory itself, below the sign-in path: a test never
creates or updates an account, never counts toward the sign-in ban and
doesn't depend on which domains use the directory. A password hash a
directory returns is never sent back. OIDC directories report their
discovered issuer; they take no passwords.

For server-level administrators with directory update permission. The
console's guided directory setup uses it to test a real person before
any domain is switched over.
2026-09-28 12:38:58 -07:00
jcoffey-dev e61a475859 Schema: each expression field says which values and variables it accepts
ci / fork-checks (pull_request) Successful in 52s
ci / build (pull_request) Successful in 5m24s
The registry knows, for every expression field, the constants it may
evaluate to and the variables its conditions may read, and enforces both.
The schema served to INBUXA Admin described every one as a bare
x:Expression, so the console could offer nothing better than free text.

tools/fork/expr-schema.py reads those contexts from the generated registry
code and writes them onto each field's type as
expression: {constants, variables}. All 124 expression fields are covered.
CI runs it with --check so the schema can't drift from the registry.
2026-09-28 12:32:28 -07:00
jcoffey-dev 6c862e4971 Release 2026.9.28.4
ci / fork-checks (pull_request) Successful in 15s
ci / build (pull_request) Successful in 23m29s
The personal-data catalog and what it feeds: the data inventory and its
snapshots (#83, #89), the Compliance Officer roles (#88), the Compliance
Overview and Data Inventory menu entries (#92). Log file retention
(#87), privacy defaults for new installs (#85, #90), webhooks that send
only the events they name (#82), and upstream v0.16.24 (#84), whose
spam rules updates keep what an admin edited.

Explain: 12 settings asked about (upstream's new createdAt fields, the
certificate dates and the webhook events policy), with the release's
recommended model built locally; 705 answers carry over.
2026-09-28 12:22:34 -07:00
jcoffey-dev 5a73a1183a Merge pull request 'Compliance menu: Overview and Data Inventory first' (#92) from feature/compliance-pages-nav into main
ci / fork-checks (push) Successful in 50s
ci / build (push) Successful in 33m2s
2026-09-28 18:48:52 +00:00
jcoffey-dev ac204078eb Merge pull request 'New installs start with the hashed-address blocklist off, and DNSBL zones read right' (#90) from feature/d5-msbl-off into main
ci / fork-checks (push) Successful in 15s
ci / build (push) Canceled after 16m10s
2026-09-28 18:32:41 +00:00
jcoffey-dev 728586998b Catalog: what the Overview showed wrong
ci / fork-checks (pull_request) Successful in 19s
ci / build (pull_request) Successful in 44m19s
Seen in the console's first Overview. x:DmarcTroubleshoot and
x:SpamClassify are one-off actions whose results come back in the
response, not kept: object-life, not unbounded. Tasks go when done
(only a failed one's status may stay, still unconfirmed): object-life.
x:Log reads the log files, so it follows inbuxa:LogSettings.keepForDays.
x:TracerLog, x:WebHook and the OpenTelemetry tracers are configuration:
their credential fields stay classified, but they are no longer listed
in the inventory, where they counted as always sent off the server
even with none configured; the log-file, webhooks and otel-tracer
sources carry what they send.
2026-09-28 11:03:55 -07:00
jcoffey-dev cca49de92c Compliance menu: Overview and Data Inventory first
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Canceled after 9m28s
Personal-data catalog spec, §8: the Compliance section in the console
reads Overview, Data Inventory, Legal Holds, Audit Log, Locked
Accounts. The two new entries are hand-built console pages
(CustomComponent/ComplianceOverview, CustomComponent/DataInventory),
shown to people with sysComplianceGet.

A console from before these pages shows the links and answers
"Unknown component", so the console that has them should be deployed
with the server release that carries this. Schema edited as the fork's
earlier Compliance entries were, hash updated.
2026-09-28 10:54:32 -07:00
jcoffey-dev b20b09f81a New installs start with the hashed-address blocklist off, and DNSBL zones read right
ci / fork-checks (pull_request) Successful in 1m3s
ci / build (pull_request) Successful in 1h11m16s
Personal-data catalog spec, default D5 (settled 2026-09-28; built after
the v0.16.24 import's spam-rules loader landed). msbl.org's EBL is sent
a SHA-1 of every email address it's asked about. A new install's first
boot now leaves a note, and the rules update, once the bundled rules
are in, switches STWT_MSBL_EBL_EMAIL off and forgets the note, so it
happens once; the loader keeps that switch through later updates. An
existing server has no note and keeps every blocklist as it is.

Also fixes the data inventory's DNSBL endpoints: a zone is an
expression (`ip_reverse + '.zen.spamhaus.org'`, conditional branches,
`hash(email, 'sha1') + '.ebl.msbl.org'`), and the zone names are now
the quoted literals that start with a dot, from every branch, rather
than the expression's text.

Tested: unit test for the zone rule; the compliance system test (no
note, no change; the inventory lists ebl.msbl.org, not a hash; with the
note the blocklist goes off; the note works once); the system suite;
fork checks.
2026-09-28 10:21:15 -07:00
jcoffey-dev 7bbbff0648 Merge pull request 'Evaluate the personal-data catalog: the data inventory and its history' (#89) from feature/data-inventory into main
ci / fork-checks (push) Successful in 36s
ci / build (push) Successful in 43m2s
2026-09-28 17:15:10 +00:00
jcoffey-dev a8fb10458b Evaluate the personal-data catalog: the data inventory and its history
ci / fork-checks (pull_request) Successful in 57s
ci / build (pull_request) Successful in 15m6s
Personal-data catalog spec, §6 (Phase 3c).

inbuxa:DataInventory/get evaluates the catalog against the server's
live settings and says what this server holds: for each source and each
object that can hold personal data, its categories and whose data it
is, whether it is collected here at all, what bounds its retention (the
live value of the setting that does, or unbounded), whether it leaves
the host and to which endpoints, and a summary. Every host that
receives something is listed once as a candidate processor with what it
receives. Inside a tenant it answers with the tenant's slice and none
of the server's processors. Read-only, with sysComplianceGet.

inbuxa:InventorySnapshot/get is the history: a dated copy of the
evaluated inventory, recorded when it changes -- after a registry write
to an object the inventory reads, after inbuxa's log, audit or AI
settings change, and on the daily clean-up -- and kept as long as the
audit log's records. ids: null lists every snapshot, newest first; the
full inventory only when asked for.

The catalog is embedded and parsed at start (new dependency: toml,
MIT/Apache); the evaluation is a pure function of it and the live
facts, so each configuration is tested without a server. Loopback
endpoints stay on the host; any other configured endpoint leaves it.

Tested: unit tests for the evaluation (a new install's defaults, an
external blob store, a hosted AI endpoint, telemetry off, a tenant's
slice, hosts from URLs, loopback), snapshots, and the fact gathering's
store and duration rules; the compliance system test, extended (the
officer reads the inventory, a plain user is refused, a tenant's
officer sees its slice and no processors, a webhook to another host
becomes a processor and a snapshot names x:WebHook, a retention change
reads through); the system, audit, legal hold and account lock suites;
fork checks. The system suite failed once of three runs with an email
import's blob not found, in antispam.rs; the same happened once in
purge.rs on the previous branch. Nothing here touches uploads; noted
for a separate look.
2026-09-28 09:59:48 -07:00
jcoffey-dev c61497e2b2 Merge pull request 'Add the compliance permission and the Compliance Officer roles' (#88) from feature/compliance-roles into main
ci / fork-checks (push) Successful in 40s
ci / build (push) Canceled after 40m52s
2026-09-28 16:34:17 +00:00
jcoffey-dev 7285b3e38a Merge main (upstream v0.16.24) into feature/compliance-roles
ci / fork-checks (pull_request) Successful in 15s
ci / build (pull_request) Successful in 7m33s
The schema conflicted as a binary file: taken from main and the one
edit here re-applied (sysComplianceGet after sysLegalHoldExport). The
import kept the permission count at 673, so the new id stays 673.

Retested on the merged tree in its own target directory: the
compliance and system suites pass. One earlier system run failed in
purge.rs (an imported blob not found) and didn't recur.
2026-09-28 09:26:27 -07:00
jcoffey-dev 9893452ca2 Merge pull request 'Merge upstream v0.16.24' (#84) from merge/upstream-v0.16.24 into main
ci / fork-checks (push) Successful in 54s
ci / build (push) Canceled after 39m0s
2026-09-28 15:55:16 +00:00
jcoffey-dev 63adb4e2b8 Add the compliance permission and the Compliance Officer roles
ci / fork-checks (pull_request) Successful in 31s
ci / build (pull_request) Successful in 11m31s
Personal-data catalog spec, §7 (settled 2026-09-28).

sysComplianceGet (673) sees the data inventory and compliance
overview: superusers and, for their tenant's slice, tenant
administrators, by default and through the one-time grants on servers
that already have their roles stored.

A Compliance Officer role at server level holds it with reading and
exporting the audit log, placing, widening, releasing and exporting
legal holds, seeing account locks, and reading accounts, lists,
domains, tenants and roles. It changes no server setting, creates or
deletes no account, and can't shorten audit retention.

A tenant's accounts can hold only roles of their own tenant (MT-3), so
the tenant role is one "Compliance Officer" role per tenant, without
holds (LH-13): made once for every tenant a server has, and whenever a
tenant is created. While nobody holds it, it is removed with its tenant
so it doesn't block the delete, and put back if the delete is refused
for another reason. Both roles carry a user's own permissions too,
since roles given to a person replace the default user role, which a
tenant's accounts can't hold anyway.

Every server makes these once, new or existing -- the built-in roles
are only made on a server with none -- and records each under P c, so a
role an administrator deletes stays deleted.

Tested: unit tests (neither role changes a setting beyond a user's
own; holds for the server's officer only; per-place records); a new
compliance system test (one server-level role; an officer reads the
audit log, places and releases a hold, and is refused a setting, an
account and audit retention; a tenant gets its role, whose holder reads
the tenant's audit log and no holds; a tenant with an unused role is
deleted and the role goes with it); the system, audit, legal hold,
account lock and SCIM suites; fork checks. The directory suite needs
its LDAP container and wasn't run here.
2026-09-28 08:50:17 -07:00
jcoffey-dev d107c1b2bb Merge pull request 'Keep rotated log files for a set number of days' (#87) from feature/log-retention into main
ci / fork-checks (push) Successful in 14s
ci / build (push) Successful in 23m17s
2026-09-28 15:27:34 +00:00
jcoffey-dev 1d5a49409f Keep rotated log files for a set number of days
ci / fork-checks (pull_request) Successful in 2m28s
ci / build (pull_request) Successful in 3m47s
Personal-data catalog spec, default D1 (settled 2026-09-28): log files
were never deleted. inbuxa:LogSettings.keepForDays says how many days
rotated log files are kept; unset (null) keeps every file, as before,
and a new install sets 30 days.

It is a fork-owned setting, stored under T + l as audit retention is,
not a field on x:TracerLog: that object is also stored inside
x:Bootstrap with a field after it, so a new field would change
x:Bootstrap's stored format. Server-level, with the tracers'
permissions (sysTracerGet, sysTracerUpdate); changes are in the audit
log, before and after.

Log files are local, so every node deletes its own: hourly, and at once
when the setting changes on that node. Only regular files named
<prefix>.<something> in each enabled log tracer's directory, last
changed more than the limit ago, are removed; the file being written is
never that old, and nothing else in the directory is touched. Minimum
one day. The catalog classifies inbuxa:LogSettings and points the log
file's retention at it.

Tested: unit tests for the file rule (only this log's old files; the
current file, other files and directories stay) and a purge on disk;
the system suite, which reads, sets, refuses zero, restores null and
checks the audit records; fork checks.
2026-09-28 08:23:36 -07:00
jcoffey-dev 80d6c09c59 Merge main into merge/upstream-v0.16.24
ci / fork-checks (pull_request) Successful in 21s
ci / build (pull_request) Successful in 35m18s
The schema, which both sides changed, merged as JSON with no conflicts.
The personal-data catalog (#83) gains upstream's new x:DnsServerPowerDns:
nothing personal but its API key, like the other DNS providers.
2026-09-28 08:19:24 -07:00
jcoffey-dev 480d93f4d6 Merge pull request 'Spec: settle where log retention lives and when D5 is built' (#86) from spec/d1-d5-settled into main
ci / fork-checks (push) Successful in 15s
ci / build (push) Canceled after 12m46s
2026-09-28 15:14:47 +00:00
jcoffey-dev f47371b3a1 Spec: settle where log retention lives and when D5 is built
ci / fork-checks (pull_request) Successful in 15s
ci / build (pull_request) Successful in 4m50s
D1 becomes a fork-owned setting, as audit retention is, because a field
on x:TracerLog would change x:Bootstrap's stored format. D5 waits for
the v0.16.24 import's reworked spam-rules loader.
2026-09-28 08:09:32 -07:00
jcoffey-dev bdd97c5828 Merge pull request 'New-install privacy defaults, and expired bans purged daily' (#85) from feature/new-install-privacy-defaults into main
ci / fork-checks (push) Successful in 1m20s
ci / build (push) Canceled after 23m15s
2026-09-28 14:51:23 +00:00
jcoffey-dev a0ffdb8071 New-install privacy defaults, and expired bans purged daily
ci / fork-checks (pull_request) Successful in 48s
ci / build (pull_request) Successful in 6m52s
Personal-data catalog spec, defaults D2, D3, D4, D6 and D7 (settled
2026-09-28, new installs only):

- D2: automatic IP bans expire after 30 days instead of never; D3:
  spam training samples, whole messages, are kept 90 days instead of
  180; D4: Pyzor, which sends a digest of each message's text to a
  public server, is off; D6: delivery history is kept 14 days instead
  of 30. Written on the first boot of a new install only -- one with no
  roles yet, the same test the built-in roles use -- by reading each
  singleton, setting these fields and writing it back whole. A server
  with roles keeps its settings, saved or default.
- D7: a webhook created from now on starts with the include policy and
  no events, so it sends nothing until events are chosen (Rust default
  and schema default, marked). The registry stores every field, so
  existing webhooks keep their policy.
- Expired bans are also removed by the daily data clean-up. They
  already stopped blocking and were deleted when settings next loaded;
  a server that seldom reloads kept them.

D1 (log retention) and D5 (the hashed-address blocklist off) are held,
and the spec says why: x:TracerLog is stored inside x:Bootstrap with a
field after it, so adding one changes that object's stored format; and
the spam-rules loader D5 touches is being reworked by the v0.16.24
import. The spec also corrects finding 3: expired bans were deleted on
settings load; bans were permanent only because no period is set.

Tested: unit tests for the new-install values and that everything else
in each singleton stays; the system suite, whose security test now
purges an expired ban and checks its record is gone; the telemetry
test; common's unit tests; fork checks.
2026-09-28 07:43:59 -07:00
jcoffey-dev 18b28fad27 Merge pull request 'Add the personal-data catalog and the check that keeps it true' (#83) from feature/privacy-catalog into main
ci / fork-checks (push) Successful in 17s
ci / build (push) Canceled after 19m3s
2026-09-28 14:32:24 +00:00
jcoffey-dev a8dde68800 Add the personal-data catalog and the check that keeps it true
ci / fork-checks (pull_request) Successful in 52s
ci / build (pull_request) Successful in 37m38s
Phase 2 of the personal-data catalog spec.

resources/privacy/catalog.toml classifies every object in the schema
(316) and inbuxa's own JMAP objects (12): each property that can hold
personal data, with its categories, and for objects that hold any,
whose data it is, where it lives, its scope and what bounds its
retention (a named setting where there is one). Twenty sources that
are no object -- the log file, exporters, webhooks, spam lookups, the
Explain cache, relays and hooks, push, legacy-use records -- carry the
same facts plus the settings that turn them on, whether the data
leaves the host, and the code that writes it. Classifications of
objects that hold data about people are from the spec's source map;
the rest are typed from the schema alone (address, IP, secret).

tools/fork/privacy-check.py fails CI when an object or inbuxa object
has no entry, when a property the schema types as an address, IP or
secret is left to its object's default, when an entry names an
object, property, setting or code path that is gone, or when it uses
a word outside the catalog's vocabulary. --unlisted prints starting
entries. strip.py's report gains "Unclassified in the privacy
catalog": objects and fields new in an import and not classified,
informational like the Enterprise flags.

Tested: 13 unit tests (tools/fork/tests): the check passes on this
tree; fails on an unclassified object, an address hidden behind a
default, a secret in a set or object reference, stale properties,
objects, settings and code paths, an unlisted inbuxa object and a
word outside the vocabulary; --unlisted's entries; and the strip
report on a synthetic import. The check and the tests run in the
fork-checks job.
2026-09-28 06:54:34 -07:00
jcoffey-dev 09c55ba503 Merge pull request 'Stop webhooks sending every event, message content included' (#82) from fix/webhook-event-levels into main
ci / fork-checks (push) Successful in 13s
ci / build (push) Successful in 38m0s
2026-09-28 13:44:51 +00:00
jcoffey-dev eac3db34e9 Principal get test: expect legacyAllowed
ci / fork-checks (pull_request) Successful in 12s
ci / build (pull_request) Successful in 1h18m28s
The per-protocol switches (#79) added legacyAllowed to the account's
urn:inbuxa:jmap capability, but this expectation wasn't updated, so
jmap_tests stopped here and the suites after it never ran.
2026-09-28 06:42:08 -07:00
jcoffey-dev 6945714aa9 Stop webhooks sending every event, message content included
ci / fork-checks (pull_request) Successful in 45s
ci / build (pull_request) Successful in 5m42s
A webhook has a level (info by default) that nothing read: its events
were chosen by its list and policy alone. With the default policy,
exclude, and nothing listed, that meant every event type, including
smtp.raw-input (the raw SMTP bytes, DATA included) and the model's
reply to the spam classifier. The docs suggest a webhook to pass the
audit log to a SIEM; set up that way it would have received whole
messages. Found by the personal-data catalog investigation (finding 1).

Now an include list is sent as named, whatever each event's level:
naming an event is the choice. Otherwise a webhook gets only events at
or above its level, as a tracer does, and never a protocol's raw input
or output (IMAP, SMTP, POP3, ManageSieve, delivery, milter), which
carries whole messages and credentials; those go out only when named.

Tested: unit tests for the rule (level, raw I/O only when named, a
named event below the level, custom event levels, a webhook's own
errors); the telemetry system test, whose webhook names debug-level
connection events and still receives them.
2026-09-28 06:38:37 -07:00
jcoffey-dev b2453d066b Merge upstream v0.16.24
Eight conflicted files resolved, plus the lock file and the schema:

- crates/services/src/task_manager/spam_classifier.rs: upstream's rules
  update now replaces existing rules, DNSBL servers, lookups and file
  extensions, keeping only whether each is on. Taken, with one difference:
  an object an admin edited is kept as it is. Every object an update writes
  is fingerprinted (content without `enable`, SHA-256, stored under
  SUBSPACE_INBUXA "Sf"), and only one that still matches is replaced.
  Scores are never replaced, as upstream has it. The AU-1.10 summary record
  now names what was added, replaced and kept, and the bundled rules are
  marked applied only when the update fully succeeded, so a failure runs
  again on the next start. The marker becomes "3.0.2+2", which runs the
  update once on upgrade to fingerprint every rule still as bundled.
- crates/common/src/network/autoconfig/autodiscover.rs: upstream's rewrite
  (implicit TLS first, labeled SSL), with the per-protocol switches (LP-7,
  LP-14a) passed in as a filter.
- crates/store/src/backend/mysql/{search,write}.rs: upstream's chunked
  deletes (no unbounded first DELETE, stop on a short chunk, halve the
  chunk on the new chunk-too-large errors) inside the fork's query timeout.
- crates/smtp/src/lib.rs: the fork's queue spawn kept. It already fixed the
  stall upstream fixes here (a node without outboundMta stops accepting
  mail at about 1024 queued messages), and follows role changes live.
- crates/jmap/src/registry/mapping/bootstrap.rs: the log path stays
  /var/log/inbuxa/; upstream's PowerDNS mapping taken.
- crates/main/Cargo.toml: the AGPL-only license kept, version 0.16.24.
- tests/src/jmap/principal/get.rs: the fork's capabilities kept.
- resources/schema/schema.json.gz: merged as JSON; upstream relabeled the
  vendor Sieve extensions "(Stalwart)", kept as "(vnd.inbuxa)".
- Cargo.lock: upstream's, with the fork's crates added by Cargo.

Also:

- tests/src/smtp/inbound/spam_rules_kept.rs: an edited rule survives an
  update, an unedited one is updated, rules from before fingerprints are
  handled, and the audit summary says so. Upstream's own spam_rules test
  passes unchanged.
- tests/src/smtp/reporting/reschedule.rs moves to port 19058; upstream's
  new spam_rules test took 19057.
- tools/fork/renames.py renames the "(Stalwart)" labels and the default
  log path, so neither conflicts again.
- tools/fork/notice-check.py compares against the newest snapshot in the
  checked-out history instead of the upstream branch head, so moving the
  branch no longer fails other open pull requests.
- tests/src/directory/issuer.rs (since v0.16.23) stays out, and is on the
  build check's known list: it tests issuer-based directory routing, which
  the fork doesn't have (DIR-2).
- Strip report: docs/fork/strip-reports/v0.16.24.{md,json}.
2026-09-28 06:30:20 -07:00
jcoffey-dev f59b084ce5 Import upstream v0.16.24, stripped
Upstream commit: af37a234981722493b74623a983581691d2b70b6
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 118 in 50 files
Dangling module declarations removed: 5
Edits turning enterprise off: 25
Third-party code: 14 files, 0 not in THIRD-PARTY.md
Renamed identifiers: 62 in 18 files
Verification: clean

The same Enterprise footprint as v0.16.23. The build check fails only on
tests/src/directory/issuer.rs, unchanged since v0.16.23: it calls a helper
from upstream's Enterprise-only OIDC test, and tests issuer-based directory
routing, an Enterprise feature. main has never carried it.
2026-09-28 06:29:38 -07:00
jcoffey-dev 85ea0c80e9 Merge pull request 'Spec: personal-data catalog, compliance role, Overview and Data inventory' (#81) from spec/personal-data-catalog into main
ci / fork-checks (push) Successful in 47s
ci / build (push) Canceled after 39m47s
2026-09-28 13:05:02 +00:00
jcoffey-dev a588a8aa7d Spec: record John's answers to the personal-data catalog questions
ci / fork-checks (pull_request) Successful in 15s
ci / build (pull_request) Successful in 7m25s
The sidecar catalog; the Compliance Officer places and releases holds;
the Tenant Compliance Officer is built now; shortening audit retention
is recorded and surfaced, not gated on a second person; all seven
new-install defaults, in Phase 3; the webhook finding fixed now as a
bug; snapshots kept as long as the audit log.
2026-09-28 05:57:24 -07:00
jcoffey-dev 35cf3f405f Spec: personal-data catalog, compliance role, Overview and Data inventory
ci / fork-checks (pull_request) Successful in 50s
ci / build (pull_request) Successful in 11m33s
Phase 1 of the GDPR auditor foundation: the investigation and the
design, committed before anything is built (SPEC.md §3 rule 3).

It maps every place the server stores or sends personal data found
in the code at de275ba, each with its categories, whose data it is,
the settings that control it, what bounds its retention, where it
lives, whether it leaves the host, its scope and the code that writes
it, and the default in a new install. It proposes a sidecar catalog
(resources/privacy/catalog.toml), since the schema and registry code
are upstream's generated output with no generator here; a CI check
modeled on name-check.py; a strip-report section; a read-only
inventory method with dated snapshots; a Compliance Officer role; and
the Compliance navigation with Overview and Data inventory.

Findings worth reading on their own: webhooks ignore levels and, at
their defaults, receive every event including raw SMTP input; log
files are never deleted; automatic bans never expire; some of the
fork's records outlive the account; spam training keeps whole
messages for 180 days; traces are on in a new install; the spam
filter sends IPs, domains, hashed addresses and body digests to
third-party services by default.

Proposed default changes (new installs only) and seven open questions
are for John to decide. No default is changed.
2026-09-28 01:36:57 -07:00
jcoffey-dev de275bac60 Merge pull request 'Release 2026.9.28.3' (#80) from release-2026.9.28.3 into main
ci / fork-checks (push) Successful in 1m1s
publish / version (push) Successful in 56s
publish / publish-amd64 (push) Successful in 30m35s
publish / release (push) Successful in 6s
ci / build (push) Successful in 32m44s
publish / publish-arm64 (push) Successful in 36m4s
publish / binaries (push) Successful in 35s
publish / announce (push) Successful in 22s
v2026.9.28.3
2026-09-28 07:23:19 +00:00
jcoffey-dev 305406a331 Release 2026.9.28.3
ci / fork-checks (pull_request) Successful in 14s
ci / build (pull_request) Successful in 7m25s
Per-protocol legacy switches (#79) and the hold export's exceptions
list (#75). The prepared Explain answers are relabeled for this
release; 706 carry over unchanged.
2026-09-28 00:15:33 -07:00
jcoffey-dev f5888d79b0 Merge pull request 'Give IMAP, POP3 and ManageSieve a switch each' (#79) from feature/per-protocol-switches into main
ci / fork-checks (push) Successful in 38s
ci / build (push) Successful in 35m58s
2026-09-28 06:32:41 +00:00
jcoffey-dev 8e9cedbe97 Give IMAP, POP3 and ManageSieve a switch each
ci / fork-checks (pull_request) Successful in 43s
ci / build (pull_request) Successful in 7m40s
The legacy-protocols switch was all or nothing. An operator can now stop
POP3 and keep IMAP: each of IMAP, POP3 and ManageSieve has its own
switch, server-wide on inbuxa:ProtocolPolicy and per tenant on
inbuxa:TenantProtocolPolicy (properties imap, pop3, manageSieve).

legacyProtocols stays as the kill-all: setting it sets all three, and it
reads "disabled" exactly when all three are off. A policy stored before
this has only legacyProtocols and reads as all three at that value, so
existing servers and tenants carry over unchanged. In one /set, a
protocol named beside legacyProtocols overrides it.

SMTP submission keeps no switch of its own: sign-in over it is refused
only when all three are off, as the single switch did (LP-6), so
turning one protocol off never stops a mail app sending. For a tenant,
the server's switches and the tenant's count together.

Server-wide, a change closes the listeners of whatever is now off and
puts back the saved listeners of whatever is on again, both in one
change if asked; listeners of a protocol still off stay saved. Sign-in,
autoconfig, autodiscover, PACC (now prepared once per combination) and
the suggested DNS records all follow each protocol separately. A tenant
may turn a protocol on only while the server has it on (LP-9), and the
refusal names which. The JMAP session adds legacyAllowed, the protocols
still allowed for the account; legacyProtocols there keeps its meaning
for older webmail builds. Events name the switches ("pop3 disabled"),
and audit before/after reads every switch even from an older policy.

Tested: unit tests for the switches, the old-policy reading, the
server/tenant combination, the tenant refusal and listener refusal; and
tests/e2e/legacy_protocols.py against a running server, all 100 checks,
including new ones: POP3 alone off closes only its port and refuses
only its sign-in while IMAP and sending go on; only POP3 stops being
advertised; one change closes IMAP and reopens POP3; a tenant turns
POP3 off for itself, and can't turn IMAP on while the server has it off.
2026-09-27 23:12:32 -07:00
jcoffey-dev 5ba54e8fb7 Merge pull request 'List what a hold export can't read instead of skipping it (LH-12)' (#75) from fix/hold-export-exceptions into main
ci / fork-checks (push) Successful in 54s
ci / build (push) Canceled after 23m21s
Reviewed-on: #75
2026-09-28 06:09:20 +00:00
jcoffey-dev db817dd507 Merge pull request 'Release 2026.9.28.2' (#77) from release-2026.9.28.2 into main
publish / version (push) Successful in 31s
ci / fork-checks (push) Successful in 52s
ci / build (push) Canceled after 9m20s
publish / publish-amd64 (push) Successful in 34m25s
publish / release (push) Successful in 45s
publish / publish-arm64 (push) Successful in 36m5s
publish / binaries (push) Successful in 34s
publish / announce (push) Successful in 22s
v2026.9.28.2
2026-09-28 05:59:59 +00:00
jcoffey-dev b7e3a765ca Release 2026.9.28.2
ci / fork-checks (pull_request) Successful in 56s
ci / build (pull_request) Successful in 5m22s
2026-09-27 22:54:18 -07:00
jcoffey-dev 7ba9ec9fa0 Merge pull request 'Send "none" instead of "pass" as the DMARC report disposition' (#76) from fix/dmarc-disposition-compat into main
ci / build (push) Canceled after 11m35s
ci / fork-checks (push) Successful in 15s
2026-09-28 05:48:22 +00:00
jcoffey-dev 4c07779c16 Merge pull request 'Recheck DNSSEC lookups that hickory wrongly calls bogus' (#72) from fix/dnssec-insecure-fallback into main
ci / fork-checks (push) Successful in 2m2s
ci / build (push) Canceled after 14m59s
2026-09-28 05:33:21 +00:00
jcoffey-dev e1e8a9aeb0 Send "none" instead of "pass" as the DMARC report disposition
ci / build (pull_request) Successful in 16m34s
ci / fork-checks (pull_request) Successful in 52s
Cloudflare's DMARC report intake rejects every aggregate report we
send with "555 5.7.1 invalid_report_schema". Bisected against the live
endpoint: the only element it objects to is <disposition>pass</disposition>,
the value RFC 9990 added for mail that passed DMARC under an enforcing
policy. The RFC 9990 namespace, <np>, <discovery_method>, <testing> and
a missing <pct> are all accepted, and a report that differs only in
using "none" there goes through.

"none" (no action taken) is valid under both RFC 9990 and RFC 7489 and
says the same thing to the reader, so reports now go out with it. The
stored report keeps "pass"; only the serialized copy changes.
2026-09-27 22:31:13 -07:00
jcoffey-dev 5c506b9d2b List what a hold export can't read instead of skipping it (LH-12)
ci / fork-checks (pull_request) Successful in 49s
ci / build (pull_request) Successful in 11m32s
An item the hold covers whose stored record or content can't be read
goes in exceptions.csv with the path it would have had and the reason,
rather than being left out silently. The file is always in the ZIP, so a
header-only one shows nothing was missed, and manifest.sha256 carries
its hash beside the manifest's.
2026-09-27 22:15:05 -07:00
jcoffey-dev 3978cf5785 Merge pull request 'Release 2026.9.28.1' (#74) from release-2026.9.28.1 into main
ci / build (push) Canceled after 29m44s
ci / fork-checks (push) Successful in 14s
publish / version (push) Successful in 32s
publish / publish-amd64 (push) Successful in 28m55s
publish / release (push) Successful in 15s
publish / publish-arm64 (push) Successful in 1h2m48s
publish / binaries (push) Successful in 51s
publish / announce (push) Successful in 23s
v2026.9.28.1
2026-09-28 05:03:38 +00:00
jcoffey-dev 815a642cc4 Release 2026.9.28.1
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 7m29s
Legal hold exports (LH-12, #73). The prepared Explain answers are
relabeled for this release; 706 carry over unchanged.
2026-09-27 21:55:55 -07:00
jcoffey-dev 1d5f4a2cd3 Merge pull request 'Export what a legal hold keeps as a ZIP (LH-12)' (#73) from feature/hold-export into main
ci / fork-checks (push) Successful in 50s
ci / build (push) Canceled after 12m21s
2026-09-28 04:51:16 +00:00
jcoffey-dev 68dd749291 Export what a legal hold keeps as a ZIP (LH-12)
ci / build (pull_request) Successful in 4m47s
ci / fork-checks (pull_request) Successful in 14s
inbuxa:HoldExport/set takes a hold, optionally some of the accounts it
covers, and a reason; the collection runs in the background and get
says when it's ready. The ZIP has, per account, mail as .eml under its
folders, calendars as .ics, contacts as .vcf, files as stored, and the
archived items the hold keeps under archived/; a manifest.csv gives each
entry's account, kind, folder, date, whether it was archived, size and
SHA-256, and manifest.sha256 hashes the manifest. Accounts the hold
doesn't cover are left out, and items outside its date range are too:
live mail by arrival, events by start, and archived items the same way,
so an export doesn't carry deleted items that only another hold keeps.

The finished file is a blob of whoever started the export, so only they
download it, and it lasts as long as any upload (uploadTtl). Exports
are records under the hold (SUBSPACE_INBUXA H/e): never changed or
destroyed, each with its status, counts, size and checksum. Starting
one needs sysLegalHoldExport, an active hold and a reason, and is
recorded in the audit log like the audit log's own export.

The build is in memory and capped at 2 GB; bigger holds fail with a
message saying so, and are split by picking accounts.

Tested: unit tests for safe ZIP names and the manifest and its hash;
the legal_hold system test, on RocksDB, PostgreSQL and MySQL, exports a
hold end to end (live and archived mail, the manifest's hash, an asked-
for account the hold doesn't cover left out) and checks the refusals
(no reason, a user without the permission, a released hold) and the
audit record; and by hand from the console on a local server. Not
covered by a test: the archived-item date range with two holds of
different ranges over one account.
2026-09-27 21:45:52 -07:00
jcoffey-dev b1bc5ed6e0 Recheck DNSSEC lookups that hickory wrongly calls bogus
ci / fork-checks (pull_request) Successful in 14s
ci / build (pull_request) Successful in 7m34s
hickory 0.26.3 rejects two kinds of valid answers, and outbound
delivery then retries those hosts until the message expires:

- A zone delegated beneath an unsigned zone (l.google.com under
  google.com). Proving the delegation insecure needs an SOA record in
  the DS reply, and public resolvers often leave it out. Every Google
  MX host behind a signed MX record was unreachable.
- A signed CNAME to a signed name that lacks the queried type. The
  NSEC denial is checked against the original name, not the target's.

On a bogus verdict, follow a signed CNAME and repeat the lookup at its
target; otherwise look up the name's zone and its parents, nearest
first. A zone that validates as unsigned means nothing below it can be
signed, so the plain resolver answers and the result is insecure. A
zone that validates as signed first leaves the verdict standing.
2026-09-27 21:45:13 -07:00