Commit Graph
25 Commits
Author SHA1 Message Date
jcoffey-dev f72e3bb85c Per-domain directories: each domain signs in against its own directory (DIR-1 to DIR-15)
The two lookups every caller uses now honor Domain.directoryId, then the
server default, then the internal directory, so sign-in, bearer routing,
recipient lookup, discovery, the PACC record and the refusal of password
changes on external accounts all follow the domain. A directoryId, or a
server default, naming a directory that doesn't exist is unavailable,
never the internal directory.

A directory speaks only for the domains it serves: an account it returns
on another directory's domain is refused, for sign-in and recipients
alike, and aliases and group claims on such domains are dropped with a
warning. A bearer token must belong to the user the client names, or the
name must be one of its aliases with alias sign-in allowed. Accounts and
groups that sync creates pass the tenant checks, limits included.
2026-09-19 10:32:57 -07:00
jcoffey-dev 3ffaee0695 SCIM: a suspended account's open sessions end, and no credential it holds still works (SCIM-52)
The push router records which account each subscription belongs to, and
a new Revoke event drops every subscription the account holds, so its
IMAP IDLE, JMAP event streams and WebSockets on this node end. Other
users' subscriptions to its shared mailboxes stay. Test 28 now checks an
open IDLE is ended, and that the account's password over HTTP and its
own API key, both already cached, are refused on the next request.
2026-09-19 09:42:03 -07:00
jcoffey-dev 940b42f3b4 SCIM: just-in-time directory sync is read-only on domains open to SCIM (SCIM-58 to SCIM-60)
On such a domain, sign-in sync creates no account (the person gets an
ordinary authentication failure), changes nothing on an existing one,
and creates no group from a groups claim. Without the flag sync works as
before, and turning it off hands accounts back to sync with no restart.
Checked through synchronize_account itself; acceptance test 5 does the
same over OIDC once per-domain directories exist.
2026-09-19 09:38:00 -07:00
jcoffey-dev 0ca26070d7 SCIM: users, groups, queries, PATCH, Bulk and cursors at /scim/v2, over x:Account (SCIM-1 to SCIM-57)
Every SCIM operation becomes the x:Account get, query or set JMAP makes,
as the service principal, so permissions, tenant scope and limits,
address uniqueness and account destruction are enforced in one place.
Discovery is anonymous; everything else takes an API key as a bearer
token and nothing else. Domains open to SCIM carry a flag in the domain
cache. Filters take eq and and, answered from the account indexes, with
unindexed attributes checked on at most 200 candidates. Cursors are
stateless, HMAC-sealed under the server key. PATCH applies to the
resource in memory and saves it as a PUT, so it is all or nothing.
Groups get an address from their display name on the principal's
domain; membership is written on each user.

Every write emits one of five new scim.* events (ids 637 to 641), also
added to the packaged schema. The helpers the surviving SCIM suites
import are rebuilt from the spec; scim_tests runs the new acceptance
suite and the surviving tenant isolation suite, and both pass.
2026-09-19 09:35:23 -07:00
jcoffey-dev 2edb552b6b Monitoring: the fork's acceptance suite (MON-1 to MON-31)
system::monitoring::monitoring_tests covers the defaults, metric
sampling, the Prometheus gauges, trace history over SMTP and LMTP
(probe sessions skipped, no raw I/O), trace destroy, indexTelemetry off,
live metrics and live tracing with their tokens and stream limit,
alerts by event and by email, and tenant admins refused. Also removes a
leftover enterprise-only attribute on the Prometheus counters loop,
which would have dropped counters from an enterprise-feature build.
2026-09-19 08:43:45 -07:00
jcoffey-dev 49e3733428 Monitoring: alerts over metrics, by event and by email (MON-25 to MON-30)
Every minute the node that calculates metrics evaluates each enabled
x:Alert, read from the registry, with metric('name') or the underscore
name. An alert fires when its condition turns true: it emits
telemetry.alert-event with the rendered message, and queues one email per
recipient through the outbound queue, placeholders filled and
Auto-Submitted set. An alert naming an unknown metric is refused on save.
The shared alerts suite runs; every telemetry suite is un-gated.
2026-09-19 08:39:17 -07:00
jcoffey-dev fc2d4f237f Monitoring: trace history, trace search, and x:Trace over the stored traces (MON-10 to MON-17, MON-32)
A lossy collector subscriber keeps each inbound SMTP session that reached
MAIL FROM and each delivery attempt, info level and above and never raw
I/O, at most 1,000 events with strings cut at 4 KiB, and writes it when
the span closes as an x:Trace under the telemetry key class, scheduling
its indexing. The index task builds a document of event types, queue ids
and keywords when indexTelemetry is on. x:Trace/get derives timestamp,
from, to and size; /query filters by opening event, text, queueId and
time; /set destroys only. The data purge honours holdTracesFor. The shared
tracing and webhook suites run.
2026-09-19 08:31:04 -07:00
jcoffey-dev 9f7035588f Monitoring: metric history, one edition of metrics, and x:Metric over the stored samples (MON-4 to MON-7, MON-9, MON-17, MON-39)
Every node writes a sample per metric on metricsCollectionInterval:
counters as the increase since its last sample, gauges always, histograms
as totals when changed. Samples are x:Metric in the registry's encoding
under the telemetry key class, ids time-ordered. x:Metric/get and /query
read them with metric and timestamp filters and full paging, hide what's
past holdMetricsFor, and the data purge deletes it. The is_enterprise
split is gone, so every gauge and histogram is collected and exported, and
queue.count is set from the queue itself. The shared metrics suite runs.
2026-09-19 08:21:45 -07:00
jcoffey-dev 9490fc4677 AI spam classification: the model's opinion as one bounded spam signal, and the llm_prompt Sieve function (AI-1 to AI-28)
The classifier sends only the subject and text, between unforgeable markers
after the operator's prompt, to an OpenAI-compatible endpoint the operator
configured; nothing is preset. Its answer maps to an LLM_ tag whose score is
clamped (+5.0, -1.0 by default) and can never discard or reject on its own;
X-Spam-LLM is sanitized, encoded and folded, and a planted one is removed.
Failures, timeouts past the ceiling, a full slot or a paused model leave
mail flowing untagged. llm_prompt answers trusted scripts, and accounts
holding interactAi within an hourly limit. Redirects aren't followed and no
content or secret is logged. The limits live in inbuxa:AiLimits.
Acceptance tests 1 and 3 to 21; test 2 as the re-enabled shared llm case,
whose setup no longer waits on a rules file from a developer's own path;
test 22 written as the ignored ai_compat.
2026-09-19 00:41:00 -07:00
jcoffey-dev 0bc6b03dcd Branding and templates: per-domain, tenant and server logos, /logo, operator calendar email templates and RSVP page (BT-1 to BT-26)
Logos resolve domain, then tenant, then server-wide, then the built-in, with
subdomains finding their domain. GET /logo serves a data-URL image, redirects
to a URL logo without fetching it, sandboxes SVG, and answers 404 when no
custom logo applies. Emails embed the first PNG, JPEG or GIF logo. Logo and
template writes are checked; stored templates are read at send time, always
escaped, and fall back to the built-in with a build warning when they don't
parse. The RSVP page is served byte for byte with a CSP and no-referrer. The
sign-in and RSVP pages load the logo through an image element. MT-22's
session logo follows the chain to the server-wide logo.
Acceptance tests 1 to 17; test 18 written as the ignored branding_compat.
2026-09-18 22:27:19 -07:00
jcoffey-dev ec4d668bc4 Multi-tenancy: impersonate has no effect inside a tenant (MT-1, MT-15)
The ceiling always disables impersonate for principals in a tenant, so no
tenant setting or grant lets them reach accounts beyond it.
2026-09-18 18:51:42 -07:00
jcoffey-dev 4a9aa9c548 Masked email: rewrite to the owner at RCPT TO, create responses carry the address, admins query all masks (ME-4, ME-9, ME-13, ME-19)
Found by running system_tests, which masked email no longer stops:
- rcpt_resolve rewrites a live mask to its owner's address, so
  Delivered-To names the account; delivery recognizes the mask from the
  original recipient when it belongs to that account.
- x:MaskedEmail/set create responses carry the server-set email.
- x:MaskedEmail/query returns every mask to a server-level impersonate
  holder, and filters on accountId.
- The refusal for an unlinked emailDomain uses upstream's wording.
- The shared delivery test checks the fork's address format (ME-13).
- The masked email test's tenant domain uses manual DKIM, so its cleanup
  leaves nothing behind.
2026-09-18 18:29:19 -07:00
jcoffey-dev 60d1d8b84a Masked email: delivery through masks (ME-4, ME-5, ME-6, ME-7, ME-9, ME-10)
A live mask accepts mail at RCPT TO and delivers to its owner, with an
X-Masked-Email header naming it. A disabled mask files straight to Trash,
past the owner's Sieve script. Deleted and expired masks are refused like
unknown addresses, without being cached as unknown. Mail moves lastMessageAt
and turns a pending mask enabled. Sub-addresses on a mask work.
2026-09-18 16:22:50 -07:00
jcoffey-dev aaca8fe537 Masked email: the fork's subspace and the masked_email module (ME-1, ME-2, ME-3, ME-6a, ME-7, ME-8, ME-13, ME-14, ME-15)
Subspace _ holds the fork's own data, with its own SQL table and RocksDB
column family, and is part of backup. The masked_email module keeps each
mask's state, last mail and pending deadline beside upstream's record, an
index from address to mask with tombstones, and a per-account change log;
and generates addresses in the fork's format.
2026-09-18 16:17:47 -07:00
jcoffey-dev 11d780b3d3 Copyright of the fork's own files is Coffey Labs's 2026-09-18 15:30:27 -07:00
jcoffey-dev ad0db8b2b0 Multi-tenancy: the inbuxa-features crate, the permission ceiling and tenant disk quota (MT-12, MT-13, MT-14, MT-15, MT-16, MT-19, MT-20)
New crate crates/features (inbuxa-features), AGPL-3.0-only, holding the
tenancy rules. Hooks in common: a tenant's roles and permission lists cap
its people's permissions; a change to a tenant, or to a role a tenant holds,
drops its members' cached permissions; delivery and every other write check
the tenant's maxDiskQuota; usedDiskQuota reads the tenant usage counter.
The default Tenant Administrator role gains sysTenantGet and sysTenantQuery.
2026-09-18 15:19:58 -07:00
jcoffey-dev 8c1879e853 Cross-origin requests only from the front ends' origins (contract C-14) 2026-09-18 13:44:55 -07:00
jcoffey-dev 31e64f5a4d Rebrand the packaged schema: defaults, labels and descriptions; the Enterprise page becomes Branding; log prefix inbuxa.log 2026-09-18 13:15:17 -07:00
jcoffey-dev 05d220ae4f No web interface on the mail host: first boot installs and downloads none (SPEC §5.3) 2026-09-18 13:10:32 -07:00
jcoffey-dev d6fc4600cd OAuth: registration required by default; first-party clients registered on every start (contract C-5, C-6) 2026-09-18 13:04:20 -07:00
jcoffey-dev faedf7a1da Versioning: INBUXA's own dated version, with the Stalwart base shown
inbuxa --version, the banner, startup events, OpenTelemetry and the JMAP
implementation string read "2026.9.18 (Stalwart 0.16.22)". The base comes
from Cargo, which keeps following upstream so version bumps merge cleanly.
Received headers and IMAP ID carry the INBUXA version.
2026-09-18 12:08:41 -07:00
jcoffey-dev c9c761fab5 Quick fixes from the first boot: recovery admin, upsell, warnings
- The recovery administrator (INBUXA_RECOVERY_ADMIN, or STALWART_RECOVERY_ADMIN)
  is honored only in bootstrap and recovery mode. On a configured server it's
  ignored with a startup warning. Before, it was a standing full-admin login
  for as long as the variable stayed set.
- The Enterprise upsell error is replaced by "This feature isn't available in
  INBUXA yet" for the features still to be rebuilt.
- Workspace warnings: 25 to 0. cargo fix removed the unused imports. The
  seven places where Enterprise code used to plug in keep their parameters,
  each with an inbuxa: comment naming the rebuild that uses it again. The
  antispam test's mock-server imports are back behind pending-rebuild.
2026-09-18 11:36:44 -07:00
jcoffey-dev d3f0b36dd2 Packaging: the binary and package are inbuxa, INBUXA_* settings with STALWART_* fallback
- crates/main: package and [[bin]] renamed to inbuxa; homepage inbuxa.org;
  license AGPL-3.0-only (upstream is dual; the fork takes the AGPL).
- types::branding::env_var reads INBUXA_<name>, falling back to
  STALWART_<name> with a warning, for all nine server settings.
  STALWART_APP_ and STALWART_SPAM_* storage keys are unchanged.
- New-install default paths /var/lib/inbuxa and /var/log/inbuxa.
- Dockerfiles, systemd unit, launchd plist and AppArmor profile renamed.
- Upstream's .github moved to .github-upstream so none of it runs.
- install.sh stubbed: upstream's would install Stalwart.
- Two missed brand strings: the SMTP Received header and the utils user agent.
2026-09-18 11:09:22 -07:00
jcoffey-dev 89665decfa Rebrand to INBUXA: product name, protocol greetings, sign-in page, calendar templates, logos, README
One branding module (types::brand!) supplies the name to every protocol
greeting, the HTTP realm, the startup banner, Received headers, the
user agent, the IMAP ID response and calendar PRODIDs. The sign-in and
calendar pages take ihasmail's palette and font and the INBUXA logo, and
calendar emails embed the INBUXA lockup. Default calendar and address book
names follow. Protocol identifiers (urn:stalwart:jmap, vnd.stalwart Sieve
extensions) and upstream copyright notices are unchanged.
2026-09-18 10:52:34 -07:00
jcoffey-dev 7dae9b29fd Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
2026-09-18 10:21:56 -07:00