Skip to content

Commit 1e62f71

Browse files
feat(service-mailbox): sys_mail_message, its record associations and the restricted unassociated pool — the inbound-mail capability (manage_mailboxes, requires: mailbox) (#22993)
Fixes #22870 Clause-②: yes (widening) PR 2 of 2 for #22870 (PR 1, `FieldSchema.mailAssociation`, landed as `e5687cb6b`). Built from the stage-0 report `6115941082` §3.1, §3.2, §3.4, §3.5 and §4 row 2, under ruling `6116489638` (Q2 → C3, Q4 → A). This PR completes the card's scope: the inbound-mail objects, their registration, both read gates, the boot wiring, `manage_mailboxes` and the `mailbox` capability token. The ingest half (`connector-imap`, ingest, dedupe, auto-reply suppression, re-match, MIME parsing, the source marker, `sys_mailbox`'s connector columns) stays with #22871. ## What changes - **New package `@objectstack/service-mailbox`** (ADR-0029 D1/D2: a capability owns its objects, registration and gates). `MailboxServicePlugin` registers three `engine-owned` objects, data API `get` and `list` only: - `sys_mail_message`: the RFC 5322 mirror (stage-0 §3.1, 18 columns). `body_html` is `textarea`, never `html`. `enable.files: true`, so attachments are `sys_attachment` rows whose gate inherits this object's gate. The unique key is `(mailbox_id, dedupe_key)` per organization. - `sys_mail_association`: one row per message-to-record link (§3.2), with the ActivityPointer pair `object_name` and `record_id` (`referenceVia: 'object_name'`). The unique key `(mail_message_id, object_name, record_id)` makes re-match idempotent. - `sys_mailbox`: the identity half only, `name`, `address` and `owner_user_id`, with `requiredPermissions: ['manage_mailboxes']`. - **Two read gates** (`mail-read-visibility.ts`, §3.4), registered as data middleware on `find`, `findOne`, `count` and `aggregate`, so a list's `total` agrees with its rows: - **Message gate.** A message is served to a caller who can read a record that one of its associations names, to the owner of its mailbox, or to a holder of `manage_mailboxes`. A pool message has no association, so an ordinary member reads it as `404 RECORD_NOT_FOUND`. - **Association gate.** A row is served to a caller who can read the record it names, or to a holder of `manage_mailboxes`. - Both use a bounded system pre-scan (2,000 rows) and fail closed. System reads are not narrowed. If the engine has no middleware seam, the plugin logs at `error` and names the consequence. - **`manage_mailboxes`** (Q4 → A) is declared in `PLATFORM_CAPABILITIES` with `scope: 'org'`. It is held by default through `ADMIN_FULL_ACCESS_CAPABILITIES` and by `organization_admin`, and so by its derived no-bypass variant. Its readers are both gates and `sys_mailbox.requiredPermissions`. - **`mailbox` capability token**: it is in `PLATFORM_CAPABILITY_TOKENS` and `PLATFORM_CAPABILITY_PROVIDERS` (open edition), and it has a `CAPABILITY_PROVIDERS` row in core. `@objectstack/cli` and `@objectstack/verify` depend on the package, so `serve` and `bootStack` mount it. The showcase now declares `requires: [..., 'mailbox']`. The token is not on the always-on slate. - **Hoisted resolver.** `resolveReadableParentIds` moves to `@objectstack/core` (`packages/core/src/security/readable-parent-ids.ts`). The plugin-audit export and the service-storage private copy are deleted, and both packages import it from core. The two bodies were byte-identical (compared before the move), so no existing gate changes its answer. - **Pins and generated files**: - the `service-mailbox` group in `platform-object-names.ts`; - three ADR-0131 inventory rows (fate `attribution`; the message is anchored to its mailbox, the association to its message); - `platform-object-tenancy-census.json`, regenerated: 87 registered, 52 in reach; - i18n bundles for en, zh-CN, ja-JP and es-ES, translated by hand. The coverage baseline is 0 untranslated; - the plugin-security grant goldens (the platform and organization admins now carry `manage_mailboxes`); - the lockstep package count (71) and the `llms.txt` package count (70); - a new row 47 on `content/docs/permissions/system-context.mdx`, for the gates' `isSystem` bypass. Later rows are renumbered, the `rows 1–62` and `row 60` references follow, and the census gate's ledger and self-test track the shift. ## Verification record (head `204aa2e8d`) - **Showcase boot through the data API**: `packages/qa/dogfood/test/mail-message-pool-read-gate.dogfood.test.ts`, 9/9. This is the §3.4 probe table, and every refusal asserts both `status` and `code`: | Probe | Measured | |---|---| | reader reads a message associated with its own contact (positive control) | `200` | | **reader reads the unassociated pool message (the pin)** | **`404`, `RECORD_NOT_FOUND`** | | reader reads a message associated only with a contact it cannot read | `404`, `RECORD_NOT_FOUND` | | reader lists the mailbox | only the associated message; `total` is 1 | | reader lists associations | only the one naming its own record | | mailbox owner reads and lists | pool served; `total` is 3 | | `manage_mailboxes` holder, and the seeded platform admin, read the pool (exemption control) | `200` | | member with no object grant reads the pool (the `403` control) | `403`, `PERMISSION_DENIED` | | the mailbox owner reads `sys_mailbox` (object grant held, capability not) | `403`, `PERMISSION_DENIED`; the holder gets `200` | - **Ablation** (scripts/ablation-replace.mjs, then a rebuild, then scripts/ablation-dist-preflight.mjs): - **Mutation.** `installMailReadVisibility` returned before registering either middleware. The marker reached `dist/` in 2 built files. The mutated tree's dts step failed on a narrowing the mutation itself caused, but the JS was emitted, and the preflight proves the marker is in it. - **Result.** The pin went red with 4 failed and 5 passed: the pool read `expected 200 to be 404`, the foreign read likewise, the list `total` was 2 instead of 1, and the association list likewise. - **Restore.** The blob equals HEAD, `git diff HEAD` is empty, `git status --porcelain` is empty, the marker is absent from `dist/`, and the suite is 9/9 again. - **A void first attempt.** It carried its marker only in a comment, which the build strips, so the mutated build left no marker in `dist/`. Its reading was discarded and the ablation was redone with the marker in a string literal. - **Package suites**: - `@objectstack/service-mailbox` 14/14: plugin registration, the provider identity, the capability pins, object shape, and the write-door check with its control; - `@objectstack/core` 2381; - `@objectstack/plugin-audit` 743; - `@objectstack/service-storage` 846; - `@objectstack/plugin-security` 4057 passed, 45 skipped; - `@objectstack/verify` 250; - `@objectstack/platform-objects` 1083; - `@objectstack/example-showcase` 409; - `@objectstack/cli` unit 4265, with the 2 identity-pin failures fixed and re-run; - spec `src/kernel`, `src/security`, `src/identity` and `src/system/constants`, 1738; - dogfood: authz-conformance, authz-probe-blind-spot, platform-app-object-entry-views, the attachment, comment, activity and ledger gate suites, the audit-capability suite, security-catalog-showcase and the new suite, 244 passed, 1 skipped. The attachment, comment, activity and ledger gate suites are the behavioural proof that the resolver hoist changes nothing. - **typecheck**: green for core, plugin-audit, service-storage, plugin-security, verify, service-mailbox, dogfood, example-showcase, spec and cli. - **eslint** (`--no-inline-config`) on the 42 changed `.ts`/`.mjs` files: 0 errors, 0 warnings. This is a targeted run, not a proof of narrowing; the repo-wide `pnpm lint` is CI's. - **Gates**: `node scripts/pm/dispatch-gates.mjs --commands` derived 155 commands on `204aa2e8d`, and all exit 0. The `--ran` reconciliation reports 155 derived, 155 run and 0 NOT-MEASURED, a derived zero. `check:generated`: all 14 artifacts up to date. - **`check:platform-checklist`** (outside per-PR CI) exits 1 with 8 problems on the base `e23eccb16` and the same 8 on the head; the problem sets are identical line for line. The hoist removed no symbol that the checklist anchors. - **Size**: 2891 changed lines (+2739 / -152, 58 files, generated files included), under 3000. ## Acceptance notes - **The resolver's shared home is `@objectstack/core`.** All three consumers already depend on it, and it already hosts `withoutOperationPrivateKeys`. A `ReadableParentIdsEngine` structural type keeps core free of an engine dependency. ADR-0139 symbol-anchored the old location, so its one pointer is re-anchored to `packages/core/src/security/readable-parent-ids.ts#resolveReadableParentIds`, the pointer alone with no prose change. This puts `docs/adr/**`, a Tier H path, in the diff, so the PR lands only on a maintainer approval. - **Files outside the claim's listed surface, each needed mechanically by the listed work:** - `packages/cli/package.json` and `packages/verify/package.json`: the provider table's readers must declare the provider package; - `.changeset/config.json`: the new package joins the fixed group; - `packages/qa/dogfood/test/…`: the showcase boot test lives with the other showcase boots, because a test inside the package would need `@objectstack/verify` and the example as dev dependencies, a cycle; - `packages/cli/test/serve-capability-identity.test.ts`: the identity pin; - `content/docs/permissions/system-context.mdx` and `scripts/check-system-context-census.mjs`: required by the census gate; - `packages/spec/llms.txt`, `content/docs/protocol/backward-compatibility.mdx` and `scripts/publish-smoke-pack.mjs`: package counts; - `docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md`: the anchor the hoist moved; - `packages/services/service-mailbox/README.md`: the README its `files[]` lists; - `scripts/i18n-coverage-baseline.json`: the new package's coverage entry. - **Liveness.** Per stage-0 §3.5, object columns carry no ledger rows. `manage_mailboxes` and `mailbox` land together with their readers in this PR. The `packages/spec/liveness/README.md` field-cell sentence was not ridden. - **Writer window.** Nothing writes these rows until #22871 lands. Every object is `engine-owned` with `get` and `list` only, so no author or API caller can create rows that nothing reads. - **Organization stamp.** The dogfood fixture inserts as the system with the reader's organization, the way ADR-0131 D9 requires the ingest service to stamp. The cross-organization negative test that §3.2 lists belongs to #22871's matcher. - **Labels.** No labels are written: this dispatch's write budget names none. ## 维护者速读(草稿) - **改了什么**:新增 `@objectstack/service-mailbox` 能力包。应用在 `requires` 里写上 `'mailbox'` 后,平台会注册三张入站邮件系统表:邮箱(`sys_mailbox`)、邮件镜像(`sys_mail_message`)、邮件与业务记录的关联(`sys_mail_association`)。同时装上两道读取门,并新增能力名 `manage_mailboxes`,平台管理员和组织管理员默认持有。另把一个“判断调用者能否读某条父记录”的公共函数从两个包挪进 `@objectstack/core`,ADR-0139 里指向它的那一个链接随之更新,ADR 正文不变。 - **为什么改**:入站邮件第一阶段(#22870)要求“未匹配到任何记录的邮件池默认受限,只有邮箱管理员和邮箱所有者可见,不对全组织开放”。现在普通成员只能读到与自己能看的记录关联的邮件;读池中邮件得到 404,按“不存在”处理,不泄露存在性。真正收信的 IMAP 接入属于 #22871,本 PR 不包含。 - **风险与代价(含回滚)**:三张表只在应用声明 `mailbox` 能力时才注册,不声明的部署零影响。公共函数的搬家经逐字节比对,原附件、评论、动态、审计日志的读取门行为不变,对应 dogfood 套件全绿。代价是组织管理员默认多了一个能力。回滚:revert 本 PR 即可;由于还没有写入方,不会留下数据。 - **席位意见**: - **你要做的(一个动作)**:因为本 PR 改动了 `docs/adr/**`(只是 ADR-0139 的一个链接),需要你在 PR 上点一次 Approve,之后由席位走合并队列落地。 --- _Generated by [Claude Code](https://claude.ai/code/session_013tMiyVNzCwaUhgfUe6gg8y)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c9b5b24 commit 1e62f71

58 files changed

Lines changed: 2739 additions & 152 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-mailbox': minor
4+
'@objectstack/core': minor
5+
'@objectstack/plugin-security': minor
6+
'@objectstack/plugin-audit': patch
7+
'@objectstack/service-storage': patch
8+
'@objectstack/cli': patch
9+
'@objectstack/verify': patch
10+
---
11+
12+
Inbound mail lands as a capability: `requires: ['mailbox']` registers the mirrored message `sys_mail_message`, its record links `sys_mail_association` and the mailbox directory `sys_mailbox`, with an unassociated pool restricted to mailbox owners and the new `manage_mailboxes` capability
13+
14+
Clause-②: yes (widening)
15+
16+
- **`@objectstack/spec`.** Two closed vocabularies each gain one entry; every stack and permission set that parsed before parses unchanged.
17+
- **`mailbox`** is a platform capability token (`PLATFORM_CAPABILITY_TOKENS`, provider `@objectstack/service-mailbox`, open edition). It is not on the always-on slate: an app that wants mailboxes declares `requires: ['mailbox']`.
18+
- **`manage_mailboxes`** is a curated organization capability (`PLATFORM_CAPABILITIES`), held by default by platform administrators (`ADMIN_FULL_ACCESS_CAPABILITIES`). Its holder reads the mailbox directory, and every mirrored message and association of its organization past the mail read gates, the unassociated pool included. Object grants and field-level security still apply to a holder.
19+
- **`@objectstack/service-mailbox`** (new). `MailboxServicePlugin` owns the three objects (ADR-0029 D1/D2: registered only where the capability is required) and installs two read gates on the data middleware, so a list's `total` agrees with its rows:
20+
- `sys_mail_message`, the RFC 5322 mirror of one inbound message (participants, Message-ID and threading headers, subject, text and HTML bodies, attachments as `sys_attachment` rows). A row is served to a caller who can read a record one of its associations names, to the owner of the mailbox it arrived in, or to a `manage_mailboxes` holder. A message no record matched (`association_state: 'unassociated'`) is therefore refused to an ordinary member as not found (`404 RECORD_NOT_FOUND`), never shown organization-wide.
21+
- `sys_mail_association`, one row per (message, record) link with the participant address and email field that matched. A row is served to a caller who can read the record it names, or to a `manage_mailboxes` holder.
22+
- `sys_mailbox`, the mailbox's address and owner, served only to a `manage_mailboxes` holder (`requiredPermissions`).
23+
- All three are `engine-owned` and the data API serves `get` and `list` only. The HTML body is plain text (`textarea`), never rendered markup. Nothing writes these rows yet: the IMAP connector and the ingest engine, which stamp associations from the email fields an app flags with `mailAssociation`, ship next in this package.
24+
- **`@objectstack/core`.** `resolveReadableParentIds` ("which of these parent records can the caller read", one caller-scoped engine read per parent object) is exported, so every parent-derived read gate asks the one evaluator. `CAPABILITY_PROVIDERS` gains the `mailbox` row.
25+
- **`@objectstack/plugin-security`.** `organization_admin` (and its derived no-bypass variant) holds `manage_mailboxes`: an organization administrator reads its own organization's mailboxes and pool.
26+
- **`@objectstack/plugin-audit`, `@objectstack/service-storage`.** The comment, activity, ledger and attachment gates import `resolveReadableParentIds` from `@objectstack/core` instead of carrying their own copies. The two copies were byte-identical, so no gate's answer changes.
27+
- **`@objectstack/cli`, `@objectstack/verify`.** Both depend on `@objectstack/service-mailbox`, so `objectstack serve` and `bootStack` mount the provider an app's `requires: ['mailbox']` names. The ADR-0131 organization-ownership inventory carries a row for each new object.

‎.changeset/config.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"@objectstack/service-i18n",
6666
"@objectstack/service-job",
6767
"@objectstack/service-knowledge",
68+
"@objectstack/service-mailbox",
6869
"@objectstack/service-messaging",
6970
"@objectstack/service-package",
7071
"@objectstack/service-queue",

‎content/docs/permissions/system-context.mdx‎

Lines changed: 21 additions & 20 deletions
Large diffs are not rendered by default.

‎content/docs/protocol/backward-compatibility.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Read the **Breaking?** column, not the version number: in the 17.x minors every
114114
1. **RFC (Request for Comments)** — Breaking changes are proposed as GitHub issues with the `protocol:breaking` label.
115115
2. **Deprecation** — The old behavior is deprecated in a MINOR release (see timeline above).
116116
3. **Migration Guide** — A detailed migration guide is published before the removal lands, in the release notes for the version that carries it.
117-
4. **Release** — In the 17.x minors the breaking change shipped in the next **MINOR** version, carrying a changeset entry marked `**BREAKING**`, and `scripts/check-changeset-no-major.mjs` failed a pull request that declared a `major` bump, because under lockstep one `major` would promote all 70 published packages. On the v18 line it ships in the next `18.0.0-next.N`: the guard stands aside in pre mode, so a pull request may declare a major bump. See [how a breaking stage lands on the v18 line](/docs/releases/v18#how-a-breaking-stage-lands-on-this-line).
117+
4. **Release** — In the 17.x minors the breaking change shipped in the next **MINOR** version, carrying a changeset entry marked `**BREAKING**`, and `scripts/check-changeset-no-major.mjs` failed a pull request that declared a `major` bump, because under lockstep one `major` would promote all 71 published packages. On the v18 line it ships in the next `18.0.0-next.N`: the guard stands aside in pre mode, so a pull request may declare a major bump. See [how a breaking stage lands on the v18 line](/docs/releases/v18#how-a-breaking-stage-lands-on-this-line).
118118

119119
---
120120

@@ -192,7 +192,7 @@ The `@objectstack/spec` package provides additional stability guarantees:
192192

193193
### Which surfaces this covers
194194

195-
**All of them.** It was not scoped to an experimental corner or a pre-release channel: all **70** packages published from this repository belong to a single Changesets `fixed` group, so they share one version number and one policy. No published surface was exempt.
195+
**All of them.** It was not scoped to an experimental corner or a pre-release channel: all **71** packages published from this repository belong to a single Changesets `fixed` group, so they share one version number and one policy. No published surface was exempt.
196196

197197
The convention was enforced rather than informal — `scripts/check-changeset-no-major.mjs` failed a pull request that introduced a `major` bump, because under lockstep a single `major` on one package would promote the entire stack. Changesets is now in pre mode with the tag `next`, and the guard stands aside in pre mode, so a pull request on the v18 line may declare a major bump.
198198

‎docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ classes stay on today's bounded fail-closed probe, by name. ⛔ Not a general su
6060

6161
One module decides which rows are judged and turns the answer into a WHERE
6262
(`packages/plugins/plugin-audit/src/parent-record-read-gate.ts#computeParentRecordFilter`); readability itself is one
63-
caller-scoped engine read per parent object (`packages/plugins/plugin-audit/src/comment-access-hooks.ts#resolveReadableParentIds`),
63+
caller-scoped engine read per parent object (`packages/core/src/security/readable-parent-ids.ts#resolveReadableParentIds`),
6464
so the parent's own CRUD grant, OWD/sharing and RLS decide and no second derivation exists. The bound is
6565
`packages/plugins/plugin-audit/src/parent-record-read-gate.ts#PARENT_GATE_SCAN_LIMIT` (2,000), the comment gate's
6666
`packages/plugins/plugin-audit/src/comment-access-hooks.ts#READ_SCAN_LIMIT` reused. The consumers, as read on

‎examples/app-showcase/objectstack.config.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ export default defineStack({
103103
// blueprint flow to auto-create a writable "app package" home
104104
// (ADR-0033 zero-package app building) and the Studio package
105105
// selector to list DB packages.
106-
requires: ['ui', 'automation', 'approvals', 'messaging', 'triggers', 'job', 'marketplace', 'webhooks', 'realtime'],
106+
// • mailbox — MailboxServicePlugin: the inbound-mail objects
107+
// (`sys_mailbox`, `sys_mail_message`, `sys_mail_association`)
108+
// and their read gates, served through the data API.
109+
requires: ['ui', 'automation', 'approvals', 'messaging', 'triggers', 'job', 'marketplace', 'webhooks', 'realtime', 'mailbox'],
107110

108111
// Concrete connectors for the `connector_action` node. The baseline engine
109112
// ships the dispatch node + an empty registry; these plugins populate it.

‎packages/cli/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"@objectstack/service-cache": "workspace:*",
9191
"@objectstack/service-datasource": "workspace:*",
9292
"@objectstack/service-job": "workspace:*",
93+
"@objectstack/service-mailbox": "workspace:*",
9394
"@objectstack/service-messaging": "workspace:*",
9495
"@objectstack/service-package": "workspace:*",
9596
"@objectstack/service-queue": "workspace:*",

‎packages/cli/src/utils/organization-ownership-inventory.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,12 @@ export const ORGANIZATION_OWNERSHIP_INVENTORY: readonly InventoryEntry[] = [
521521
{ object: 'sys_secret', census: P1, fate: 'attribution', citation: `re-verification 6018712820: sys_secret is tenant-attributed by one producer (moved here from C6); ${D10_SINGLE}`, anchors: [] },
522522
{ object: 'sys_scim_connection_credential', census: P1, fate: 'attribution', citation: `${D3_ORG_ROWS}; ${D10_SINGLE}`, anchors: [] },
523523
{ object: 'sys_flow_credential', census: PMAIN, fate: 'attribution', citation: `${CENSUS_MAIN} (in reach, added since the first census); ${D10_SINGLE}`, anchors: [] },
524+
// Inbound mail (`@objectstack/service-mailbox`): every row is written by the
525+
// system with the mailbox's organization stamped (ADR-0131 D9), so a NULL
526+
// row has no producer today; the parents below are where one would take it.
527+
{ object: 'sys_mailbox', census: PMAIN, fate: 'attribution', citation: `${CENSUS_MAIN} (in reach, added since the first census); ${D10_SINGLE}`, anchors: [] },
528+
{ object: 'sys_mail_message', census: PMAIN, fate: 'attribution', citation: `${CENSUS_MAIN} (in reach); the message's mailbox carries the organization`, anchors: [parent('mailbox_id', 'sys_mailbox')] },
529+
{ object: 'sys_mail_association', census: PMAIN, fate: 'attribution', citation: `${CENSUS_MAIN} (in reach); the association's message carries the organization`, anchors: [parent('mail_message_id', 'sys_mail_message')] },
524530
{ object: 'sys_import_job', census: P1, fate: 'attribution', citation: `${D3_ORG_ROWS}; ${D10_SINGLE}`, anchors: [] },
525531
{ object: 'sys_user_preference', census: P1, fate: 'attribution', citation: `${D3_ORG_ROWS}; ${D10_SINGLE}`, anchors: [] },
526532

‎packages/cli/test/serve-capability-identity.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ const EXPECTED_PROVIDER_NAME: Record<string, string> = {
123123
// PackageServicePlugin registers, which the drift block below re-derives.
124124
'package-registry': 'package-service',
125125
email: 'com.objectstack.service.email',
126+
mailbox: 'com.objectstack.service.mailbox',
126127
sms: 'com.objectstack.service.sms',
127128
sharing: 'com.objectstack.service.sharing',
128129
'pinyin-search': 'com.objectstack.plugin.pinyin-search',

‎packages/core/src/capability-providers.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,13 @@ export const CAPABILITY_PROVIDERS: Readonly<Record<string, CapabilitySpec>> = {
181181
export: 'EmailServicePlugin',
182182
identities: ['com.objectstack.service.email', 'EmailServicePlugin'],
183183
},
184+
// Inbound mail: `sys_mailbox`, `sys_mail_message`, `sys_mail_association`
185+
// and their read gates. Requested, never always-on (ADR-0029 D2).
186+
mailbox: {
187+
pkg: '@objectstack/service-mailbox',
188+
export: 'MailboxServicePlugin',
189+
identities: ['com.objectstack.service.mailbox', 'MailboxServicePlugin'],
190+
},
184191
sms: {
185192
// #2780 — backs phone-number OTP sign-in/reset (plugin-auth) and
186193
// the messaging `sms` channel. Provider config lives in the `sms`

0 commit comments

Comments
 (0)