jcoffey-dev is traveling from Thursday 1 October through Sunday 4 October. Issues and pull requests are welcome, and will get an answer after that. Thanks for your patience.
All six settled as recommended. Answer 6 ("and any other recognized and protected PII") becomes a catalog of identifiers with published formats and checks, grouped by region, each either checked by its check digit or counted only beside a corroborating word, plus templates named for what they find. Data with no number to find is covered by word lists and not claimed as detection. Office documents are read; PDF counts as can't be inspected.
Phase 1: one native rule engine at DATA, after the system Sieve script,
for both DLP policies and transport rules. DLP checks outgoing mail
with counted detectors (payment cards, IBAN, US SSN, word lists,
patterns) and blocks, warns with an audited override, or holds for
review. Held mail stays in the queue unscheduled, with its own review
record, so the queue's stored format is unchanged. Matches go to the
audit log without the matched text. Six questions for John at the end.
The automation suite's DNS test compared the published zone with one
copied from upstream v0.16.22. Its _ua-auto-config record carries a
SHA-256 of the account-configuration (PACC) document, and that document
names the provider as the brand, which the rebrand changed. The digest
the server publishes is right; the expected zone still held upstream's.
With the new digest the whole automation suite passes: ACME (including
the not-due reschedule check), DKIM, DNS and RFC 2136. It had been
failing at this point on main since the rebrand.
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.
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.
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
9 changed files with 626 additions and 7 deletions
# Feature spec: data loss prevention and mail flow rules
Status: **approved 2026-09-28**, with the answers under [Settled](#settled)
and the detector catalog in §2.3. Phase 1 of DLP and the rule builder, specced together because they
need the same conditions, the same place in the mail path and the same record
of what matched. Not a rebuild of an upstream feature, so it has no line in
SPEC.md §4's table.
## Provenance
Written for the record SPEC.md §3 rule 3 asks for. Sources, and nothing else:
| Source | License | Used for |
|---|---|---|
| This repository at `0502eb4` (2026-09-28): `crates/smtp/src/inbound/data.rs`, `crates/smtp/src/queue/`, `crates/jmap/src/submission/set.rs`, `crates/common/src/scripts/`, `vendor/sieve-rs`, `resources/schema/schema.json.gz` | AGPL-3.0-only | Where a check can run, what the queue stores, what a sender sees on a refusal |
| inbuxa-admin at `b82904c` | AGPL-3.0-only | Where the pages go |
| ihasmail-inbuxa (the webmail) at `290bc63` | AGPL-3.0-or-later | How a refused send reaches the person sending |
| `inbuxa-drafts/queue/dlp.md`, `rule-builder.md` | Own | What John asked for and settled |
| The personal-data catalog spec and the audit-hold-lock spec | Own | Roles, the audit log, legal holds, the catalog check |
| RFC 5321, RFC 3463 (enhanced status codes), RFC 8620/8621 (JMAP) | Public | Refusal codes and the submission error shape |
| The issuing authorities' published formats and check-digit rules for each identifier in §2.3 (ISO 13616, ISO/IEC 7812, ISO 7064, and each national scheme's own publication) | Public | The detector rules; each is implemented from its publication and tested against its published examples |
No Enterprise-only file or snippet was used, and no third-party DLP product
was consulted for design: the detectors are public checksum and format rules
(Luhn, ISO 13616 mod 97, the SSA's published SSN rules).
## What it is
1.**Data loss prevention (DLP).** Policies that look at mail as someone
sends it, find what shouldn't leave (card numbers, bank accounts, national
ID numbers, words and patterns an organization names, attachments of a
kind), and then **block** it with a notice, **warn** and let the sender
send anyway with a stated reason, or **hold** it until a reviewer releases
or rejects it.
2.**Mail flow rules.** The same engine, for ordinary transport rules an
administrator writes in a form instead of in Sieve: disclaimers, banners,
headers, copies, redirects, refusals.
3.**One record of what matched**, in the audit log, and a **review queue**
for held mail.
Settled before this spec (John, 2026-09-27 and 2026-09-28): DLP's first
version checks **outgoing mail only**, content and attachments, as it's sent;
its actions are block with a notice, warn with an override (audited), and hold
for review. A record-only action was not chosen. The rule builder goes
alongside DLP, one design; journaling comes after both.
**Out of scope** (later specs): inbound DLP, files and calendar sharing,
scanning mail already stored, a machine-learning classifier (the local AI
model could add one later; nothing here depends on it), mobile and ihasmail
screens, journaling.
Nothing in code, docs, UI text or output claims the product meets a legal
standard or prevents every leak. The pages say what a policy checks and what
it did.
## 1. What exists today
Checked by reading the code at `0502eb4`:
| Need | Today |
|---|---|
| A place in the send path that sees every outgoing message | Yes. SMTP submission and webmail sends both reach `Session::queue_message` (`inbound/data.rs`); JMAP submission builds a local session and runs MAIL, RCPT and DATA (`jmap/src/submission/set.rs` ~L690–760). Nothing reaches the queue around it. |
| Order at DATA | Authentication checks → spam filter → milters → MTA hooks → the DATA system Sieve script → headers, DKIM signing → queue. |
| Rules without code | System Sieve (`x:SieveSystemScript`): one script per stage, chosen by an expression on `x:MtaStageData.script`. Hand-written only; the console has a text field. |
| Refusing a message | A 5xx at DATA. The webmail gets `forbiddenToSend` with the text `Server rejected DATA: <reply>` and nothing structured. |
| Holding a message | **Nowhere.** "Quarantine" in the code is only DMARC's disposition. The queue's recipient status is `Scheduled`, `Completed`, `TemporaryFailure`, `PermanentFailure`, archived with rkyv. |
| Reading attachments | Text and HTML parts only. There's no PDF or Office text extraction: search indexes text parts and file names. |
| Recording what happened | The audit log, with system actors, reasons and outcomes (AU-1…AU-12). |
| Who is allowed | Roles with per-permission grants; server and tenant levels; the compliance roles from the catalog spec. |
## 2. Design
### 2.1 One engine, native, after the system script
Rules are evaluated by a new native engine at DATA, **after** the system Sieve
script and before headers and DKIM signing. Mail flow rules and DLP policies
are two views of the same rule list.
Why not generate Sieve: holding a message, a warning the sender can override,
counted detectors with checksums, and a per-rule match record are all things
Sieve doesn't have. Adding them means new extensions in `vendor/sieve-rs`,
which widens the fork of a crate we'd otherwise take from upstream, and a
generated script would have to share the single DATA script with whatever an
administrator wrote by hand. A native engine leaves hand-written Sieve exactly
as it is: it still runs, first, and the rules see its result.
The engine lives in `crates/features/src/mailflow/` (pure evaluation over a
parsed message and an envelope, unit-testable), called from `data.rs` behind
one `// inbuxa:` marked block.
### 2.2 Rules
A fork-owned JMAP object, `inbuxa:MailRule`, stored in inbuxa's own subspace
like legal holds (not a registry object, so upstream schema imports never
touch it):
| Property | |
|---|---|
| `name`, `description` | |
| `kind` | `dlp` or `transport`: which page shows it and which permission edits it |
| `enabled` | |
| `priority` | Order; lower runs first |
| `direction` | `outgoing` (authenticated senders), `incoming`, or `any`. **DLP rules are `outgoing` only** in this version. |
| `conditions` | All must match (list below) |
| `exceptions` | Any matching one skips the rule |
| `actions` | What happens (list below) |
| `stopProcessing` | Later rules don't run for this message |
| `tenantId` | Always none in this version: rules are server-level (settled answer 3); a **Tenant** condition narrows a rule to tenants |
| `createdBy`, `updatedAt` | |
Every create, update and delete is audited with its before and after, like
any setting, and appears on the compliance Overview's **Changes that affect
review**.
### 2.3 Conditions
Shared by both kinds:
| Condition | Matches when |
|---|---|
| Sender | the sender is one of the chosen accounts, or in a chosen group, domain or tenant |
| Tenant | the sender is in one of the chosen tenants |
| Recipient | any recipient is one of the chosen addresses, domains, groups |
| **Recipient outside** | any recipient isn't at a domain this server hosts |
| Subject or body contains | any of a list of words or phrases (whole words, case-insensitive) |
| Subject or body matches | a regular expression (the `regex` crate: linear time, no backtracking) |
| Header | a header exists, or its value contains or matches |
| Attachment | its detected type, extension or name matches; its size is over a limit; there are more than N |
| **Can't be inspected** | an attachment is encrypted or password-protected (ZIP, PDF, Office), or bigger than the inspection limit |
| Message size | over a limit |
DLP adds **detectors**. Each counts what it finds, and a rule sets a minimum
(for example "5 or more card numbers"). A detector is one of two strengths:
- **Checked**: the identifier has a published check digit or checksum, so a
random number rarely passes. Found on its own.
- **Needs a word**: the format is too common to trust alone (nine digits, a
date). Counted only with a corroborating word nearby, within 50 characters
either side, in the languages where the identifier is used ("passport",
"Reisepass", "pasaporte"...).
The catalog (settled answer 6: the recognized, protected identifiers, not a
chosen few). Each row is one table entry and one check function in
`crates/features/src/mailflow/detectors/`:
| Region | Detector | Strength | Rule |
|---|---|---|---|
| Any | Payment card number | Checked | 13–19 digits, spaces or dashes allowed, a known issuer prefix (ISO/IEC 7812), Luhn |
| Any | IBAN | Checked | country code, length for that country, ISO 13616 mod 97 |
| Any | SWIFT/BIC | Needs a word | 8 or 11 characters, a valid country code in positions 5–6 |
| Any | Email addresses, in bulk | Checked | a count of distinct addresses (a customer list leaving), not one address |
| Any | Phone numbers, in bulk | Needs a word | a count of distinct numbers in international or national form |
| Any | Date of birth | Needs a word | a date beside "born", "DOB", "date of birth" and their translations |
| Any | Passport number | Needs a word | the formats of the issuing countries in this table |
| Any | Private key | Checked | a PEM or OpenSSH private-key block |
| Any | Cloud and service credentials | Checked | the published prefixes and lengths: AWS access key IDs, GitHub tokens, Slack tokens, Stripe live secret keys, Google API keys |
| US | Social Security number | Checked | `AAA-GG-SSSS`, or nine digits with a word; never area 000, 666 or 9xx, group 00, serial 0000 |
| US | ITIN | Checked | 9XX-GG-SSSS with the IRS's group ranges |
| US | EIN | Needs a word | a valid IRS prefix and seven digits |
| US | Bank routing number (ABA) | Checked | nine digits, a valid Federal Reserve prefix, the 3-7-1 checksum |
| US | Driver's license | Needs a word | each state's published format |
| US | Medicare Beneficiary Identifier | Checked | CMS's 11-character pattern and excluded letters |
| US | National Provider Identifier | Checked | ten digits, Luhn over the `80840` prefix |
| US | DEA registration number | Checked | two letters, seven digits, DEA's check digit |
| UK | National Insurance number | Checked | two letters (HMRC's excluded prefixes), six digits, A–D |
| UK | NHS number | Checked | ten digits, mod 11 |
| UK | Unique Taxpayer Reference | Needs a word | ten digits |
| Canada | Social Insurance Number | Checked | nine digits, Luhn |
| Australia | Tax File Number | Checked | weighted mod 11 |
| Australia | Medicare number | Checked | ten digits, weighted check digit |
| EU | Germany: tax ID (Steuer-ID) | Checked | eleven digits, ISO 7064 MOD 11,10 |
| EU | Germany: ID card number | Checked | nine characters, the 7-3-1 check digit |
| EU | France: social security number (NIR) | Checked | fifteen characters, mod 97 key |
| EU | Spain: DNI and NIE | Checked | eight digits and the mod 23 letter |
| EU | Italy: codice fiscale | Checked | sixteen characters, the check letter |
| EU | Netherlands: BSN | Checked | nine digits, the eleven test |
| EU | Belgium: national number | Checked | eleven digits, mod 97 |
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
usecrate::utils::server::TestServer;
@@ -58,7 +61,7 @@ _995._tcp.pop3.example.org. IN TLSA 2 1 1
_dmarc.example.org. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]"
_mta-sts.example.org. IN TXT "v=STSv1; id=12942536112359691423"
_smtp._tls.example.org. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"
_ua-auto-config.example.org. IN TXT "v=UAAC1; a=sha256; d=9X2mMgWAc10oSPuRKZSFBwPXEQpnxkS7SXPO8PC7euM="
_ua-auto-config.example.org. IN TXT "v=UAAC1; a=sha256; d=ZZ35kyyCO86LM5UUTecwutQ8B+0XdZ3wJnjoYXnH0Wk="
_validation-persist.example.org. IN TXT "pebble.letsencrypt.org; accounturi=REDACTED"
dummy-v1-ed25519._domainkey.example.org. IN TXT "v=DKIM1; k=ed25519; h=sha256; p=REDACTED"
dummy-v1-rsa._domainkey.example.org. IN TXT "v=DKIM1; k=rsa; h=sha256; p=REDACTED"
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.