Commit Graph
207 Commits
Author SHA1 Message Date
jcoffey-dev 1c1838af05 Release 2026.9.30.1
github/ci (branch) GitHub Actions
publish / version (push) Skipped
publish / publish-amd64 (push) Skipped
publish / publish-arm64 (push) Skipped
publish / release (push) Skipped
publish / binaries (push) Skipped
ci / github (pull_request) Successful in 6m45s
announce / announce (release) Successful in 10s
publish / github (push) Failing after 1h16m13s
publish / announce (push) Skipped
github/ci (tag) GitHub Actions
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
2026-09-30 13:45:36 -07:00
jcoffey-dev d9754c46a6 Release 2026.9.30
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
publish / version (push) Skipped
publish / publish-amd64 (push) Skipped
publish / publish-arm64 (push) Skipped
publish / release (push) Skipped
publish / binaries (push) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 11m5s
github/ci (tag) GitHub Actions
publish / github (push) Failing after 49m30s
publish / announce (push) Skipped
2026-09-30 12:04:16 -07:00
jcoffey-dev 20abf69d31 x:Metric: say which node wrote each sample
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m5s
Each node stores histograms as running totals since it started. A sample
didn't say which node wrote it (the node was only in the id's low bits),
so a reader couldn't diff totals per node, and the console diffed across
nodes: on the three-node production cluster the delivery attempt time
read 14.7 s over the last hour against 0.7 s from the nodes' own figures.

x:Metric/get now returns nodeId alongside timestamp, both from the id.
The telemetry suite checks every sample carries it.
2026-09-30 11:43:13 -07:00
jcoffey-dev f1f112fc38 Release 2026.9.29.2
ci / fork-checks (pull_request) Successful in 52s
ci / build (pull_request) Successful in 16m40s
2026-09-29 10:10:08 -07:00
jcoffey-dev a5c8927dbc Merge pull request 'Take a token, never a password, outside DAV' (#122) from feature/http-basic-dav-only into main
ci / fork-checks (push) Canceled after 7s
ci / build (push) Canceled after 7s
2026-09-29 17:04:01 +00:00
jcoffey-dev e147206e82 Release 2026.9.29.1
ci / fork-checks (pull_request) Successful in 50s
ci / build (pull_request) Successful in 13m48s
2026-09-29 08:25:13 -07:00
jcoffey-dev ca6484c356 Honor the client registration override only in setup and recovery
The recovery administrator signs in before any OAuth client is
registered, so it needs to skip the registration check. Outside
bootstrap and recovery mode, every account now signs in through a
registered client and one of its redirect URIs.
2026-09-29 08:25:13 -07:00
jcoffey-dev faf3d1e056 Take a token, never a password, outside DAV
ci / fork-checks (pull_request) Successful in 17s
ci / build (pull_request) Successful in 7m41s
Anyone could host a copy of a front end on a server of their own,
collect a person's password there, and replay it as HTTP Basic against
JMAP or the API. Cross-origin rules don't stop that, since a server
isn't a browser, and neither does client registration, since Basic
never goes through OAuth (contract C-23).

JMAP (session, API, upload, download, event source, WebSocket), /api,
/auth/introspect, /auth/userinfo and authenticated /auth/register now
refuse an Authorization: Basic header before looking at the password,
with a 401 whose only challenge is Bearer. A wrong password gets the
same answer as the right one. CalDAV and CardDAV keep Basic, and their
401s still offer it. The sign-in page's /api/auth takes the password in
its body and is unaffected, as is the token endpoint's client
authentication.

Bootstrap and recovery mode accept Basic everywhere, as they keep
permissive CORS. INBUXA_HTTP_BASIC_AUTH=all puts it back everywhere;
dav is the default, and any other value logs a warning and keeps it.
Test builds accept Basic everywhere, since the integration suites sign
in with passwords, and legacy_protocols.py sets the variable.

Tested: unit tests for the paths, and tests/e2e/http_basic_auth.py
against the debug build, 26 checks, including both front ends' sign-in
path and a refused unregistered redirect.
2026-09-29 07:02:05 -07:00
jcoffey-dev f1f05db790 Release 2026.9.29
ci / fork-checks (pull_request) Successful in 46s
ci / build (pull_request) Successful in 4m19s
2026-09-28 22:38:59 -07:00
jcoffey-dev 6ee7ba1b7e Merge pull request 'Logs: a total only when it's known, not the query cap' (#117) from fix/log-query-total into main
ci / fork-checks (push) Successful in 17s
ci / build (push) Canceled after 23m14s
2026-09-29 05:00:18 +00:00
jcoffey-dev daa486f7e7 Journaling: search, read and export over JMAP, and the chain check
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 8m0s
Phase 4 of the journaling spec.

- inbuxa:JournalEntry/query and /get (sysJournalSearch): filter by time,
  sender, recipient, either, direction, subject words, Message-ID and
  journal, newest first; the whole report only when asked for.
- inbuxa:JournalExport/set (sysJournalExport): a reason is required; a
  ZIP of the matching reports with manifest.csv, exceptions.csv and
  manifest.sha256, up to 10,000 reports and 1 GB.
- inbuxa:JournalVerification/set (sysJournalGet): chains and reports
  rechecked.
- Every search, listing, read, export and check is written to the audit
  log before anything is returned, with existing actions only.
- Catalog entries for the three objects; spec as-built notes.

journal_tests: administrators can't search; a Compliance Officer searches,
lists, reads a report, exports (reason required) and checks the chain;
the officer can't change journals; each of those is in the audit log.
2026-09-28 21:45:09 -07:00
jcoffey-dev 9c29fb2bea Logs: a total only when it's known, not the query cap
ci / fork-checks (pull_request) Successful in 55s
ci / build (pull_request) Successful in 17m3s
2026-09-28 21:42:53 -07:00
jcoffey-dev abd5811420 Merge pull request 'Journaling: outside archives, and Journal it in mail flow rules' (#116) from feature/journal-archive into main
ci / fork-checks (push) Successful in 50s
ci / build (push) Canceled after 19m30s
2026-09-29 04:33:59 +00:00
jcoffey-dev 64550ebbd0 Journaling: outside archives, and Journal it in mail flow rules
ci / fork-checks (pull_request) Successful in 1m19s
ci / build (pull_request) Successful in 5m44s
Phase 3 of the journaling spec.

- A journal's destination: builtIn (true for journals stored before) and
  archiveAddress, at least one. Reports to an archive are queued from the
  empty sender, one per address, flagged so they're never journaled.
- A pending record per report. When the queue lets go of one without
  delivering it (refused, expired, deleted), it becomes its own entry in
  the built-in journal under the sending journals' retention, the
  journal's archiveFailures (count, last time, reason) goes up, and the
  audit log records it; if that can't be written it stays queued.
- Journal it: a rule action naming a journal, on mail flow rules and
  beside a DLP rule's block, warn or hold. A journal whose scope chooses
  nobody takes only what rules send it.
- The report lists recipients a rule added or redirected to under
  "Added by rule", by rule name.
- A rule's route is cleared between messages in one SMTP session, with the
  new journal marks; a second message used to keep the first one's route.

tests/src/system/journal.rs: destination validation, a rule-only journal
fed by a rule that also adds a recipient, an unreachable archive's report
kept in the built-in journal with the failure counted, a report delivered
to an archive here and not journaled itself.
2026-09-28 21:27:44 -07:00
jcoffey-dev eea96e8674 Security to-do list: accepted items, kept on the server
ci / fork-checks (pull_request) Successful in 19s
ci / build (pull_request) Successful in 8m5s
2026-09-28 21:22:43 -07:00
jcoffey-dev 441ad0b18e Journaling: capture at the queue, the built-in journal, retention
ci / fork-checks (pull_request) Successful in 41s
ci / build (pull_request) Successful in 8m2s
Phase 2 of the journaling spec.

- A copy of each message is taken in MessageWrapper::queue, after DLP and
  transport rules, for every enabled journal that takes it (direction and
  scope: everyone, or accounts, groups, domains, tenants). If the copy
  can't be taken the message isn't queued (temporary failure).
- The journal report: the envelope one field a line (sender, To, Cc, Bcc
  from the envelope, list members from their ORCPT, direction, held for
  review), then the queued message byte for byte as message/rfc822.
- The built-in journal under J in the inbuxa subspace: one chain per node
  whose links name each entry by SHA-256, so entries can expire out of
  chain order; purge leaves a marker, and verify catches an entry changed
  or removed early and a report that doesn't match.
- Retention per journal (30 to 3650 days); an entry keeps what it was
  written with. The daily maintenance purges what's due, keeping entries
  whose people a legal hold covers (deleted accounts a hold keeps too),
  and records the counts in the audit log.
- inbuxa:Journal get/set, audited by the request layer. Permissions
  680-683: administrators see and change journals; the Compliance Officer
  sees, searches and exports. Whoever changes journals may grant search and
  export without holding them, so officers can still be appointed.
- Catalog entries (inbuxa:Journal, source "journal"); spec as-built notes.

tests/src/system/journal.rs: validation, internal mail with a Bcc,
outgoing into two journals, incoming over LMTP, the report and its
original, tamper and early removal caught, hold-aware purge, retention
changes leave entries alone, disabled and removed journals take nothing.
2026-09-28 20:46:04 -07:00
jcoffey-dev 823d42d528 Mail rules: group and tenant ids in their JMAP form
ci / fork-checks (pull_request) Successful in 2m26s
ci / build (pull_request) Successful in 5m47s
senderGroup, senderTenant and recipientGroup conditions now read and write group and tenant ids as JMAP ids ("b", "c"…), like legal hold scopes and the rest of the API, so the console can use its object pickers; plain numbers are still read. Held as numbers for matching. Unit test for both forms and a bad id; mail_rules_tests round-trips a tenant condition over JMAP.
2026-09-28 19:30:28 -07:00
jcoffey-dev de514115dd DLP: how long held mail waits is a setting
ci / fork-checks (pull_request) Successful in 51s
ci / build (pull_request) Successful in 4m32s
inbuxa:DlpSettings (singleton, urn:inbuxa:jmap): keepHeldDays, 1 to 90,
7 by default (settled answer 5 made it a setting). sysDlpPolicyGet reads
it, sysDlpPolicyUpdate changes it, server-level, audited by the request
layer. Each held message keeps the days it was given, and the sender's
notices say that number. Privacy catalog entry; spec §2.6 updated.

mail_rules_tests: 7 by default, 0 refused, 3 set and a message held
afterwards expires 3 days after it was held, the expiry notice says 3.
2026-09-28 19:27:27 -07:00
jcoffey-dev b59eebf1e7 Submissions say when DLP held the message
ci / fork-checks (pull_request) Successful in 44s
ci / build (pull_request) Successful in 4m44s
An EmailSubmission create's response carries inbuxa:held (dlp-and-mail-flow-rules spec, §2.6, §4): true when the message is held for review, false otherwise, so the webmail can say so at once. A sender can't read the review queue, and a held message's sendAt is its real send time, not the century-off release, so this is how the sender learns. mail_rules_tests checks both values.
2026-09-28 18:48:39 -07:00
jcoffey-dev f44382fb09 DLP phase 3: hold for review
ci / fork-checks (pull_request) Successful in 33s
ci / build (pull_request) Successful in 9m58s
The hold action now holds (dlp-and-mail-flow-rules spec, §2.6), where
until now it blocked.

- At DATA a hold decision queues the message with its release a century
  off (the queue's future-release mechanism, so the stored format is
  unchanged and an older node just never sends it), transport rules
  still applied, and replies 250 Held for review. A review record under
  R/h + queue id keeps the sender, recipients, subject, size, rules and
  detector counts. The sender is told when the rule asks.
- smtp/queue/held.rs: release (each recipient due now, its next notice
  as far off as it was, its lifetime counted from the release), reject
  (removed from the queue, the sender told, with the reviewer's note),
  and expiry: the daily clean-up rejects what nobody reviewed in 7 days,
  recorded as the server's doing.
- inbuxa:HeldMessage get/set: the review queue, sysDlpReviewGet to list
  and read (preview, 64 KB of text, only when asked for and recorded as
  blobAccess), sysDlpReviewUpdate to release or reject, a reason
  required and audited by the request layer; no create or destroy;
  server-level only.
- Guards: Emails > Queue refuses to change or delete held mail; the
  sender can't unsend it.
- Privacy catalog entry for inbuxa:HeldMessage; spec §2.6 as built.

Tests: mail_rules_tests gains the whole flow (held and listed with
counts, sender notified and nothing delivered, queue and unsend
refused, preview recorded, reject needs a reason and tells the sender
the note, release delivers, expiry returns it, decisions audited with
reasons). smtp inbound, system_tests (after one BlobNotFound in
antispam, the known flake, then clean), features and common unit tests.
2026-09-28 18:33:12 -07:00
jcoffey-dev dd73e0ad74 Merge pull request 'Mail flow rules: carry out the transport actions' (#106) from feature/mailflow-actions into main
ci / fork-checks (push) Successful in 14s
ci / build (push) Canceled after 26m39s
2026-09-29 01:17:03 +00:00
jcoffey-dev 7f045c626a Merge pull request 'DLP at DATA: block, warn and override over SMTP and JMAP' (#104) from feature/dlp-data-stage into main
ci / fork-checks (push) Successful in 15s
ci / build (push) Canceled after 16s
2026-09-29 01:16:45 +00:00
jcoffey-dev 7f22006e97 Mail flow rules: carry out the transport actions
ci / fork-checks (pull_request) Successful in 50s
ci / build (pull_request) Successful in 4m52s
Phase 2g of the DLP and mail flow rules spec: transport rules now act,
on outgoing and incoming mail.

- features/mailflow/rewrite.rs: add or remove a header, prefix or set the
  subject (an RFC 2047 word when not ASCII), add a disclaimer. A
  disclaimer edits the message's main text and HTML bodies only, each
  decoded, changed and written back as UTF-8 quoted-printable with its
  other headers kept, top or bottom (after <body> or before </body> in
  HTML); attachments and attached messages are left alone, and a
  disclaimer already present isn't added again.
- smtp/inbound/mailflow.rs: the check runs for incoming mail too
  (transport rules only; DLP stays outgoing). After DLP passes, each
  matched transport rule's actions run in order: message edits,
  add-recipient and redirect (envelope changes DATA applies), route (a
  per-message queue ahead of the queue strategy), refuse (550 5.7.1
  with the rule's text). The override tag is stripped with the same
  subject writer, so a non-ASCII subject stays valid.
- Audit: refusals and changes to where mail goes are recorded (sender,
  or system:mail-flow for incoming mail); wording and header changes
  aren't, or a banner rule would record every message (spec §2.7).

Tests: rewrite unit tests (headers, encoded subjects, disclaimers on a
single part and on multipart/alternative with an attachment, once
only); mail_rules_tests gains the actions end to end: disclaimer,
header and subject prefix on a delivered message, a redirect, a
refusal, a banner on incoming LMTP mail that outgoing rules leave
alone, and which of those are audited.
2026-09-28 18:08:40 -07:00
jcoffey-dev e35fc3e6d6 Ports: each node checks the others' ports from outside
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 7m26s
2026-09-28 18:07:49 -07:00
jcoffey-dev 5f52dad5f1 Merge pull request 'Explain: don't prepare answers for date fields' (#101) from fix/explain-skip-date-fields into main
ci / fork-checks (push) Successful in 37s
ci / build (push) Canceled after 8m58s
2026-09-29 01:06:46 +00:00
jcoffey-dev 15064d6fd5 Merge pull request 'Webhooks: send one sample event to a saved webhook' (#100) from feature/webhook-test into main
ci / fork-checks (push) Canceled after 34s
ci / build (push) Canceled after 33s
2026-09-29 01:06:13 +00:00
jcoffey-dev e0060c9e6e DLP at DATA: block, warn and override over SMTP and JMAP
ci / fork-checks (pull_request) Successful in 49s
ci / build (pull_request) Successful in 23m36s
Phase 2f of the DLP and mail flow rules spec: the rules now run on mail
an authenticated sender submits, after the DATA system script and
before headers and DKIM signing (§2.1).

- smtp/inbound/mailflow.rs: builds what the rules look at from the
  message (subject, the text version of each body, one level of attached
  messages, attachment text via the extractor, 10 MB of text at most)
  and the envelope (sender's groups and tenant; each recipient local or
  not, and its groups). Skipped entirely when no enabled rule applies to
  outgoing mail. Rules that can't be loaded refuse with a 451: nothing
  unchecked leaves.
- Block: 550 5.7.1 with the rule's notice. Warn: 550 5.7.1 with the
  notice and how to override: "[override: reason]" at the start of the
  subject, taken out before the message goes on (settled answer 1).
  Until phase 3, a hold rule blocks rather than let mail through.
- JMAP: EmailSubmission takes inbuxa:dlpOverride {reason}; a refusal
  comes back as inbuxa:dlpWarning or inbuxa:dlpBlocked with each rule's
  name and notice (description too, for older clients).
- Audit: one record per DLP match, the sender as actor, action create,
  target a message: the recipient domains, each rule with its detectors'
  counts, the outcome, an override's reason. Never the matched text. No
  new audit action: an older node that meets one fails its daily
  clean-up, which would make rolling back unsafe (spec §2.7 updated).

Tests: mail_rules_tests gains the DLP flow over JMAP (no rules, warning
with rule and notice, local recipient not warned, override with a
reason, block that no reason passes, the subject tag stripped from the
delivered message, audit records with no card or key text). smtp
inbound tests pass; system_tests passed twice after one timeout in the
email delivery tests that didn't recur.
2026-09-28 17:52:37 -07:00
jcoffey-dev 8afaee7d21 DLP and mail flow rules: inbuxa:MailRule over JMAP, and its permissions
ci / fork-checks (pull_request) Successful in 15s
ci / build (pull_request) Successful in 4m41s
Phase 2e of the DLP and mail flow rules spec, the API half.

- inbuxa:MailRule/get and /set under urn:inbuxa:jmap. Rules convert
  through serde, so what a client sends is the stored format. A create
  or change is validated whole (Rule::validate) and refused with the
  property at fault; id, createdBy, createdAt and updatedAt are the
  server's. Every change goes through the request layer's audit record.
- Six permissions, ids 674-679 (enum and schema labels): mail flow rules
  (sysMailRuleGet/Update), DLP rules (sysDlpPolicyGet/Update) and held
  mail (sysDlpReviewGet/Update, for phase 3). Either kind's permission
  gets through the gate; the handler shows and changes each rule only
  with its own kind's. All server-level: a tenant is refused (settled
  answer 3).
- Administrators get all six; the server-level Compliance Officer gets
  DLP rules to see and held mail to review (settled answer 4), added
  once to an existing server's officer role by the grant mechanism,
  which gains an officer audience.
- Privacy catalog entry for inbuxa:MailRule.

tests/src/system/mail_rules.rs: create, list in order, validation,
server-set properties refused, update, kind-separated permissions for
an officer, destroy, audit records.
2026-09-28 17:29:35 -07:00
jcoffey-dev c8280de9c3 DLP and mail flow rules: the rule model, the engine and the node cache
Phase 2e of the DLP and mail flow rules spec, in the features crate.

- rules.rs: a rule (§2.2) with its conditions (§2.3) and actions (§2.4),
  as JSON under R/r in the fork's subspace. validate() enforces the
  spec's shape: DLP rules check outgoing mail and have exactly one of
  block, warn or hold; transport rules have neither those nor
  detectors; lists, header names, header values (one line), addresses,
  texts, word lists, patterns and detector ids are checked.
- engine.rs: rules compiled once (word lists to automata, patterns to
  size-limited regexes) and run in priority order with exceptions and
  stop processing. Each detector runs at most once per message and
  only when a rule asks for it. The outcome lists what matched with
  each detector's count, and decides DLP strictest first: block, hold,
  warn; an override answers warnings only (§2.5).
- cache.rs: each node's compiled copy, refreshed after 30 seconds or at
  once when this node changes a rule.

Nothing calls this yet: the JMAP object and the check at DATA follow.
55 unit tests in mailflow.
2026-09-28 17:29:35 -07:00
jcoffey-dev 92d14fbd60 DLP: regional identifiers and templates
ci / fork-checks (pull_request) Successful in 1m47s
ci / build (pull_request) Successful in 7m42s
Phase 2b of the DLP and mail flow rules spec: every identifier in the
§2.3 catalog, each implemented from its issuer's published rules and
tested against published examples.

US (SSN, ITIN, EIN, ABA routing, driver's licenses, MBI, NPI, DEA), UK
(NI number, NHS number, UTR), Canada (SIN), Australia (TFN, Medicare),
the EU (Germany's tax ID and ID card, France's NIR, Spain's DNI/NIE,
Italy's codice fiscale, the Dutch BSN, Belgium's national number,
Poland's PESEL, Sweden's personnummer, Denmark's CPR, Finland's HETU,
Ireland's PPS, Portugal's NIF, Austria's SVNR), Norway, Switzerland,
India (Aadhaar, PAN), China, Japan, Singapore, South Korea, Brazil (CPF,
CNPJ), Mexico (CURP) and South Africa. 49 detectors in all, plus seven
templates named for what they find.

An identifier that is only digits and whose check about one random
number in ten passes counts alone only in its written form
(536-22-1234, 943 476 5919) and as bare digits only beside a word; ABA
routing numbers and NPIs always need one. Spec §2.3 records this.

A test runs every detector over an ordinary business email (order,
invoice and tracking numbers, dates, amounts, an address) and requires
nothing to fire but the contact detectors. 47 unit tests.
2026-09-28 17:13:42 -07:00
jcoffey-dev 01f6b99631 Merge pull request 'DLP: the detector framework, the region-free detectors, word lists and attachment text' (#99) from feature/dlp-detectors into main
ci / fork-checks (push) Successful in 2m37s
ci / build (push) Canceled after 8m29s
2026-09-29 00:13:20 +00:00
jcoffey-dev 8d5e4ee052 Explain: don't prepare answers for date fields
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 3m54s
2026-09-28 17:11:32 -07:00
jcoffey-dev 9e0aab6b6a Webhooks: send one sample event to a saved webhook
ci / fork-checks (pull_request) Successful in 52s
ci / build (pull_request) Successful in 18m39s
2026-09-28 17:02:47 -07:00
jcoffey-dev dc49bf4d14 DLP: the detector framework, the region-free detectors, word lists and attachment text
ci / fork-checks (pull_request) Canceled after 8s
ci / build (pull_request) Canceled after 8s
Phase 2a of the DLP and mail flow rules spec: pure functions in
crates/features/src/mailflow, nothing wired into the mail path yet.

- Detectors report distinct values found, each either checked by its
  published check digit or counted only beside a corroborating word
  within 50 characters. This PR adds the region-free ones: payment
  cards (issuer prefixes, Luhn), IBAN (registry lengths, mod 97),
  SWIFT/BIC, email addresses and phone numbers in bulk, dates of birth,
  passport numbers, private keys and published service-token formats.
  Regional identifiers follow, a region per PR.
- Word lists (Aho-Corasick, whole words, any case) and patterns (regex
  with a compiled-size limit) count occurrences.
- Attachment text: text files with or without a UTF-16 mark, HTML,
  DOCX/XLSX/PPTX, ODT/ODS/ODP and ZIP archives one level deep, read
  with the zip and quick-xml crates the workspace already has.
  Encrypted files, PDF, legacy binary Office files, nested archives
  and anything past the limits come back as not inspectable, with why.

21 unit tests, against the networks' test card numbers and the IBAN
registry's own examples among others.
2026-09-28 17:00:49 -07:00
jcoffey-dev 3199a6f1fb Release 2026.9.28.5
ci / fork-checks (pull_request) Successful in 47s
ci / build (pull_request) Successful in 7m37s
2026-09-28 16:57:47 -07:00
jcoffey-dev ba75ab4ecc Merge pull request 'ACME: a renewal that isn't due yet is rescheduled, not failed for good' (#93) from fix/acme-not-due-reschedule into main
ci / fork-checks (push) Successful in 18s
ci / build (push) Successful in 42m1s
2026-09-28 21:52:17 +00:00
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 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 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 beb6c33e63 ACME: a renewal that isn't due yet is rescheduled, not failed for good
ci / fork-checks (pull_request) Successful in 14s
ci / build (pull_request) Successful in 16m46s
When a valid certificate already covered a domain's names (one stored
by hand before the domain was switched to automatic, for instance), the
renewal task ended with NotDue, which the task manager treats as a
permanent failure. Nothing rescheduled it, so the certificate expired
unrenewed. The renewal now returns a new AcmeRenewal task due when the
certificate falls due, the same way a successful renewal does, and logs
it as a backoff.

The ACME integration suite checks that renewing again right after
issuance hands back one AcmeRenewal for that domain, due at the
certificate's renewal point.
2026-09-28 12:15:05 -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 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 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 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 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 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