Repository navigation
feat(email): add dedicated system email graph - #1162
Conversation
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
📝 WalkthroughWalkthroughThis change adds dedicated operator-owned system-email graph tables, copies valid legacy records, reports parity, and supports atomic reconciliation. It also updates account export, retention, migration tracking, and admin mailbox maintenance interfaces. ChangesSystem email graph expansion
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Admin as Admin MCP action
participant Maintenance as Mailbox maintenance
participant Repo as System email graph repository
participant Legacy as Legacy email tables
participant Dedicated as Dedicated system email tables
Admin->>Maintenance: submit system_email_graph_reconcile with force=true
Maintenance->>Repo: reconcile graph from legacy
Repo->>Legacy: read validated authoritative records
Repo->>Dedicated: delete drift and upsert records
Repo->>Repo: reload parity report
Repo-->>Maintenance: return metrics and parity
Maintenance-->>Admin: return audited reconciliation result
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
|
🔎 Preview deployed: https://kody-pr-1162.kody-a99.workers.dev Worker: Mocks:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/worker/src/email/system-email-graph-repo.ts (1)
301-322: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDerive the batch result offsets from the contract list.
mutationCountsanddeletedMutationCountsboth encode the statement layout as literal indices, and the caller passes the literal offset4at line 361. The mapping is correct today becausesystemEmailGraphColumnContractshas exactly four entries in threads → messages → attachments → deliveryEvents order. If a fifth table joins the contract, all three sites break silently and report metrics against the wrong table. Key the counts off the contract array instead.♻️ Proposed refactor to key metrics by contract
-function mutationCounts( - results: ReadonlyArray<D1Result<unknown>>, - offset: number, -): SystemEmailGraphMutationCounts { - return { - threads: Number(results[offset]?.meta.changes ?? 0), - messages: Number(results[offset + 1]?.meta.changes ?? 0), - attachments: Number(results[offset + 2]?.meta.changes ?? 0), - deliveryEvents: Number(results[offset + 3]?.meta.changes ?? 0), - } -} - -function deletedMutationCounts( - results: ReadonlyArray<D1Result<unknown>>, -): SystemEmailGraphMutationCounts { - return { - threads: Number(results[3]?.meta.changes ?? 0), - messages: Number(results[2]?.meta.changes ?? 0), - attachments: Number(results[1]?.meta.changes ?? 0), - deliveryEvents: Number(results[0]?.meta.changes ?? 0), - } -} +function mutationCounts( + results: ReadonlyArray<D1Result<unknown>>, + order: ReadonlyArray<SystemEmailGraphColumnContract>, + offset: number, +): SystemEmailGraphMutationCounts { + const counts: SystemEmailGraphMutationCounts = { + threads: 0, + messages: 0, + attachments: 0, + deliveryEvents: 0, + } + for (const [index, contract] of order.entries()) { + counts[contract.key] = Number(results[offset + index]?.meta.changes ?? 0) + } + return counts +}Then update the caller:
const results = await input.db.batch<unknown>(statements) const postReport = await loadSystemEmailGraphParityReport({ db: input.db }) return { metrics: { - deleted: deletedMutationCounts(results), - upserted: mutationCounts(results, 4), + deleted: mutationCounts(results, childFirst, 0), + upserted: mutationCounts( + results, + systemEmailGraphColumnContracts, + childFirst.length, + ), referencedOwnerMismatchCount: referencedOwnerMismatchCount(postReport), }, postReport, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/worker/src/email/system-email-graph-repo.ts` around lines 301 - 322, Update mutationCounts, deletedMutationCounts, and their caller to derive result positions from systemEmailGraphColumnContracts instead of literal indices and the hard-coded offset 4. Preserve the existing threads, messages, attachments, and deliveryEvents mapping while making it automatically follow the contract list order and length when additional tables are added.packages/worker/src/email/system-email-graph-repo.node.test.ts (1)
255-285: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAdd coverage for the message-delete cascade into attachments.
The drift scenario deletes a thread row and an attachment row, but never a
system_email_messagesrow.system_email_attachments.message_iddeclaresON DELETE CASCADEinpackages/worker/migrations/0130-system-email-graph-expand.sqlat lines 102-103. When delete-drift removes a dedicated message, SQLite cascades intosystem_email_attachments, and those cascaded rows are not counted in the statementchangesvalue thatdeletedMutationCountsreads. The reporteddeleted.attachmentsmetric then under-reports, and that metric reaches operators throughadmin_mailbox_maintenance.Seed a legacy message that becomes invalid while it still has a dedicated attachment, then assert both the resulting row state and the reported metric. This pins the intended metric semantics before step 4b.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/worker/src/email/system-email-graph-repo.node.test.ts` around lines 255 - 285, Extend the reconcileSystemEmailGraphFromLegacy test scenario to seed a legacy message that becomes invalid while retaining a dedicated attachment, causing message deletion to cascade to system_email_attachments. Assert the cascaded attachment is absent from the final database state and that repaired.metrics.deleted.attachments includes the cascaded deletion, alongside the existing row-state and metric assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/worker/src/app/account-retention-dispositions.ts`:
- Around line 34-57: Update the step 4a retention flow and its account-retention
disposition handling to actively clean dedicated system-email tables instead of
marking them only as alternate_cleanup. Apply the existing 90-day and
5,000-message retention rules to system_email_messages and
system_email_delivery_events, and remove orphaned system_email_attachments and
system_email_threads using the established cleanup helpers. Add an integration
test that inserts dedicated records and verifies the cleanup executes before
step 4b.
---
Nitpick comments:
In `@packages/worker/src/email/system-email-graph-repo.node.test.ts`:
- Around line 255-285: Extend the reconcileSystemEmailGraphFromLegacy test
scenario to seed a legacy message that becomes invalid while retaining a
dedicated attachment, causing message deletion to cascade to
system_email_attachments. Assert the cascaded attachment is absent from the
final database state and that repaired.metrics.deleted.attachments includes the
cascaded deletion, alongside the existing row-state and metric assertions.
In `@packages/worker/src/email/system-email-graph-repo.ts`:
- Around line 301-322: Update mutationCounts, deletedMutationCounts, and their
caller to derive result positions from systemEmailGraphColumnContracts instead
of literal indices and the hard-coded offset 4. Preserve the existing threads,
messages, attachments, and deliveryEvents mapping while making it automatically
follow the contract list order and length when additional tables are added.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d9567dc5-838c-4df5-9110-22fb8421bf13
📒 Files selected for processing (18)
docs/contributing/architecture/data-storage.mdpackages/worker/migrations/0130-system-email-graph-expand.sqlpackages/worker/src/account/data-targets.node.test.tspackages/worker/src/account/data-targets.tspackages/worker/src/admin/mailbox-maintenance.node.test.tspackages/worker/src/admin/mailbox-maintenance.tspackages/worker/src/app/account-retention-dispositions.node.test.tspackages/worker/src/app/account-retention-dispositions.tspackages/worker/src/email/outbound-provider-index.node.test.tspackages/worker/src/email/outbound-provider-index.tspackages/worker/src/email/system-email-graph-columns.tspackages/worker/src/email/system-email-graph-migration.node.test.tspackages/worker/src/email/system-email-graph-repo.node.test.tspackages/worker/src/email/system-email-graph-repo.tspackages/worker/src/email/system-email-graph-sql.tspackages/worker/src/mcp/capabilities/admin/admin-mailbox-maintenance.node.test.tspackages/worker/src/mcp/capabilities/admin/admin-mailbox-maintenance.tstools/migration-ledger.json
| { | ||
| table: 'system_email_delivery_events', | ||
| kind: 'alternate_cleanup', | ||
| reason: | ||
| 'Operator-owned D1 delivery events are excluded from account retention; step 4b will route the existing 90-day system-email retention policy to this dedicated table.', | ||
| }, | ||
| { | ||
| table: 'system_email_messages', | ||
| kind: 'alternate_cleanup', | ||
| reason: | ||
| 'Operator-owned D1 messages are excluded from account retention; step 4b will route the existing 90-day age and 5,000-message system cap to this dedicated table.', | ||
| }, | ||
| { | ||
| table: 'system_email_attachments', | ||
| kind: 'alternate_cleanup', | ||
| reason: | ||
| 'Operator-owned D1 attachment metadata follows dedicated system messages; step 4b will delete it through the routed system-email retention path.', | ||
| }, | ||
| { | ||
| table: 'system_email_threads', | ||
| kind: 'alternate_cleanup', | ||
| reason: | ||
| 'Operator-owned D1 threads are pruned when orphaned by dedicated system-message retention; routing begins in step 4b.', | ||
| }, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Add active cleanup before using alternate_cleanup.
Lines 38, 44, 50, and 56 state that cleanup routing starts in step 4b. Step 4a copies legacy records into separate dedicated tables. Legacy retention deletes cannot remove these dedicated copies. The copied messages and attachment metadata can exceed the 90-day and 5,000-message limits if step 4b is delayed.
Run equivalent retention and orphan cleanup for the dedicated tables during step 4a. Add an integration test that executes the cleanup against dedicated records.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/worker/src/app/account-retention-dispositions.ts` around lines 34 -
57, Update the step 4a retention flow and its account-retention disposition
handling to actively clean dedicated system-email tables instead of marking them
only as alternate_cleanup. Apply the existing 90-day and 5,000-message retention
rules to system_email_messages and system_email_delivery_events, and remove
orphaned system_email_attachments and system_email_threads using the established
cleanup helpers. Add an integration test that inserts dedicated records and
verifies the cleanup executes before step 4b.
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/worker/src/email/system-email.ts`:
- Around line 491-523: Guard the reconcileSystemEmailGraphFromLegacy call in
pruneSystemEmailRetention with try/catch so unexpected reconciliation failures
do not reject the already-completed retention work; record dedicatedGraphSync as
skipped with the thrown error’s reason, push a dedicated reconciliation-failure
warning, log the failure, and return the result. Extend the
SystemEmailRetentionResult skipped-reason union to include this new failure
reason, preserving the existing blob-delete-errors handling.
In `@packages/worker/src/email/test-schema.ts`:
- Around line 271-327: Update the test schema for system_email_delivery_events
to create the same unique partial provider_event_id index as the legacy table,
preserving the condition that excludes NULL values. Check migration
0130-system-email-graph-expand.sql for the canonical index name and definition,
then add the equivalent index alongside the CREATE TABLE statement in the test
schema.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 530dbe4a-5511-45b9-92ae-9aa3c70e2020
📒 Files selected for processing (8)
docs/contributing/architecture/data-storage.mdpackages/worker/src/app/account-retention-dispositions.node.test.tspackages/worker/src/app/account-retention-dispositions.tspackages/worker/src/email/system-email-graph-repo.node.test.tspackages/worker/src/email/system-email-retention-graph.workers.test.tspackages/worker/src/email/system-email.tspackages/worker/src/email/system-email.workers.test.tspackages/worker/src/email/test-schema.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/worker/src/app/account-retention-dispositions.node.test.ts
- packages/worker/src/app/account-retention-dispositions.ts
- packages/worker/src/email/system-email-graph-repo.node.test.ts
- docs/contributing/architecture/data-storage.md
| if (result.blobDeleteErrors > 0) { | ||
| result.dedicatedGraphSync = { | ||
| status: 'skipped', | ||
| reason: 'legacy-blob-delete-errors', | ||
| blobDeleteErrors: result.blobDeleteErrors, | ||
| } | ||
| result.warnings.push('dedicated-graph-sync-skipped') | ||
| console.warn('system-email-dedicated-graph-sync-skipped', { | ||
| reason: 'legacy-blob-delete-errors', | ||
| blobDeleteErrors: result.blobDeleteErrors, | ||
| }) | ||
| return result | ||
| } | ||
|
|
||
| const graphSync = await reconcileSystemEmailGraphFromLegacy({ db: input.db }) | ||
| result.dedicatedGraphSync = { | ||
| status: 'reconciled', | ||
| upserted: graphSync.metrics.upserted, | ||
| deleted: graphSync.metrics.deleted, | ||
| referencedOwnerMismatchCount: | ||
| graphSync.metrics.referencedOwnerMismatchCount, | ||
| parity: graphSync.postReport.parity, | ||
| } | ||
| if (!graphSync.postReport.parity) { | ||
| result.warnings.push('dedicated-graph-post-reconcile-mismatch') | ||
| console.warn('system-email-dedicated-graph-post-reconcile-mismatch', { | ||
| referencedOwnerMismatchCount: | ||
| graphSync.metrics.referencedOwnerMismatchCount, | ||
| upserted: graphSync.metrics.upserted, | ||
| deleted: graphSync.metrics.deleted, | ||
| }) | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Guard reconcileSystemEmailGraphFromLegacy against unexpected failures.
Line 505 calls reconcileSystemEmailGraphFromLegacy with no try/catch. If this call throws (D1 batch failure, constraint violation, transient error), the entire pruneSystemEmailRetention promise rejects. All legacy retention work already committed above (age-based deletes, over-cap deletes, delivery-event deletes, thread cleanup) is lost from the caller's view, and no warnings entry or dedicatedGraphSync status records the failure.
This differs from the blobDeleteErrors > 0 branch, which explicitly records a skipped status, pushes a warning, and returns instead of throwing. An unhandled reconciliation error causes a harder failure than the exact case this function was written to tolerate gracefully.
Wrap the reconciliation call the same way, and extend the skipped reason union in SystemEmailRetentionResult (Lines 358-362) to cover this case.
🛡️ Proposed fix to guard the reconciliation call
dedicatedGraphSync:
| {
status: 'reconciled'
upserted: SystemEmailGraphMutationCounts
deleted: SystemEmailGraphMutationCounts
referencedOwnerMismatchCount: number
parity: boolean
}
| {
status: 'skipped'
- reason: 'legacy-blob-delete-errors'
+ reason: 'legacy-blob-delete-errors' | 'reconcile-error'
blobDeleteErrors: number
}- const graphSync = await reconcileSystemEmailGraphFromLegacy({ db: input.db })
- result.dedicatedGraphSync = {
- status: 'reconciled',
- upserted: graphSync.metrics.upserted,
- deleted: graphSync.metrics.deleted,
- referencedOwnerMismatchCount:
- graphSync.metrics.referencedOwnerMismatchCount,
- parity: graphSync.postReport.parity,
- }
- if (!graphSync.postReport.parity) {
- result.warnings.push('dedicated-graph-post-reconcile-mismatch')
- console.warn('system-email-dedicated-graph-post-reconcile-mismatch', {
- referencedOwnerMismatchCount:
- graphSync.metrics.referencedOwnerMismatchCount,
- upserted: graphSync.metrics.upserted,
- deleted: graphSync.metrics.deleted,
- })
- }
+ try {
+ const graphSync = await reconcileSystemEmailGraphFromLegacy({
+ db: input.db,
+ })
+ result.dedicatedGraphSync = {
+ status: 'reconciled',
+ upserted: graphSync.metrics.upserted,
+ deleted: graphSync.metrics.deleted,
+ referencedOwnerMismatchCount:
+ graphSync.metrics.referencedOwnerMismatchCount,
+ parity: graphSync.postReport.parity,
+ }
+ if (!graphSync.postReport.parity) {
+ result.warnings.push('dedicated-graph-post-reconcile-mismatch')
+ console.warn('system-email-dedicated-graph-post-reconcile-mismatch', {
+ referencedOwnerMismatchCount:
+ graphSync.metrics.referencedOwnerMismatchCount,
+ upserted: graphSync.metrics.upserted,
+ deleted: graphSync.metrics.deleted,
+ })
+ }
+ } catch (error) {
+ result.dedicatedGraphSync = {
+ status: 'skipped',
+ reason: 'reconcile-error',
+ blobDeleteErrors: 0,
+ }
+ result.warnings.push('dedicated-graph-sync-skipped')
+ console.warn('system-email-dedicated-graph-sync-skipped', {
+ reason: 'reconcile-error',
+ error,
+ })
+ }Do you want me to open an issue to track this and add a corresponding test case that simulates a reconciliation failure?
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (result.blobDeleteErrors > 0) { | |
| result.dedicatedGraphSync = { | |
| status: 'skipped', | |
| reason: 'legacy-blob-delete-errors', | |
| blobDeleteErrors: result.blobDeleteErrors, | |
| } | |
| result.warnings.push('dedicated-graph-sync-skipped') | |
| console.warn('system-email-dedicated-graph-sync-skipped', { | |
| reason: 'legacy-blob-delete-errors', | |
| blobDeleteErrors: result.blobDeleteErrors, | |
| }) | |
| return result | |
| } | |
| const graphSync = await reconcileSystemEmailGraphFromLegacy({ db: input.db }) | |
| result.dedicatedGraphSync = { | |
| status: 'reconciled', | |
| upserted: graphSync.metrics.upserted, | |
| deleted: graphSync.metrics.deleted, | |
| referencedOwnerMismatchCount: | |
| graphSync.metrics.referencedOwnerMismatchCount, | |
| parity: graphSync.postReport.parity, | |
| } | |
| if (!graphSync.postReport.parity) { | |
| result.warnings.push('dedicated-graph-post-reconcile-mismatch') | |
| console.warn('system-email-dedicated-graph-post-reconcile-mismatch', { | |
| referencedOwnerMismatchCount: | |
| graphSync.metrics.referencedOwnerMismatchCount, | |
| upserted: graphSync.metrics.upserted, | |
| deleted: graphSync.metrics.deleted, | |
| }) | |
| } | |
| if (result.blobDeleteErrors > 0) { | |
| result.dedicatedGraphSync = { | |
| status: 'skipped', | |
| reason: 'legacy-blob-delete-errors', | |
| blobDeleteErrors: result.blobDeleteErrors, | |
| } | |
| result.warnings.push('dedicated-graph-sync-skipped') | |
| console.warn('system-email-dedicated-graph-sync-skipped', { | |
| reason: 'legacy-blob-delete-errors', | |
| blobDeleteErrors: result.blobDeleteErrors, | |
| }) | |
| return result | |
| } | |
| try { | |
| const graphSync = await reconcileSystemEmailGraphFromLegacy({ | |
| db: input.db, | |
| }) | |
| result.dedicatedGraphSync = { | |
| status: 'reconciled', | |
| upserted: graphSync.metrics.upserted, | |
| deleted: graphSync.metrics.deleted, | |
| referencedOwnerMismatchCount: | |
| graphSync.metrics.referencedOwnerMismatchCount, | |
| parity: graphSync.postReport.parity, | |
| } | |
| if (!graphSync.postReport.parity) { | |
| result.warnings.push('dedicated-graph-post-reconcile-mismatch') | |
| console.warn('system-email-dedicated-graph-post-reconcile-mismatch', { | |
| referencedOwnerMismatchCount: | |
| graphSync.metrics.referencedOwnerMismatchCount, | |
| upserted: graphSync.metrics.upserted, | |
| deleted: graphSync.metrics.deleted, | |
| }) | |
| } | |
| } catch (error) { | |
| result.dedicatedGraphSync = { | |
| status: 'skipped', | |
| reason: 'reconcile-error', | |
| blobDeleteErrors: 0, | |
| } | |
| result.warnings.push('dedicated-graph-sync-skipped') | |
| console.warn('system-email-dedicated-graph-sync-skipped', { | |
| reason: 'reconcile-error', | |
| error, | |
| }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/worker/src/email/system-email.ts` around lines 491 - 523, Guard the
reconcileSystemEmailGraphFromLegacy call in pruneSystemEmailRetention with
try/catch so unexpected reconciliation failures do not reject the
already-completed retention work; record dedicatedGraphSync as skipped with the
thrown error’s reason, push a dedicated reconciliation-failure warning, log the
failure, and return the result. Extend the SystemEmailRetentionResult
skipped-reason union to include this new failure reason, preserving the existing
blob-delete-errors handling.
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Summary
Adds the non-destructive step-4a deploy boundary for operator-owned email:
system_email_threads/messages/attachments/delivery_eventstablessystem:emailrows; no legacy mutation/deletionsystem_email_graph_reconcilecatch-up actionLive reads/writes remain on legacy tables until step 4b after production copy verification.
System recap — extends D1 operator email placement (medium risk)
Mode: recap · Base:
main@61874c4e· Head:a3897236Classification: extends — adds a dedicated D1 home and reconciliation gate for the existing operator-email primitive; no new top-level primitive.
Primitives touched
d1-app-dbemailrbacSystem map
Legacy system-email authority is copied and atomically reconciled into a dedicated operator graph before step-4b routing.
Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).
Invariants
force:true, audited, atomic, and followed by parity.Verification
a3897236: all required jobs pass; Bugbot passesConductor report
7787f8c9…remains recordedNote
Medium Risk
Non-destructive additive schema and copy-only path, but it touches operator mail storage, atomic reconcile batches, and retention ordering; legacy authority is unchanged until 4b, with explicit parity gates before cutover.
Overview
Introduces migration 0130 and four operator-only D1 tables (
system_email_threads,system_email_messages,system_email_attachments,system_email_delivery_events) that mirror legacysystem:emailgraph rows withoutuser_id. The migration copies valid legacy rows in FK order; legacy tables remain live read/write and retention authority until step 4b.Reconciliation and parity:
reconcileSystemEmailGraphFromLegacyruns one atomic D1 batch (child-first deletes, parent-first upserts) with fences so cross-owner inbox/sender/thread references are skipped and keep parity false. Aggregate-only parity reports (counts and mismatch classes, no message content) cover the graph plus outbound provider index dispositionlegacy-email-messages-until-4b-routing.Operations: Admin mailbox maintenance status now includes
systemEmailGraph; a new audited actionsystem_email_graph_reconcilerequiresforce: true. Scheduled system-email retention still prunes legacy first; on success it reconciles the dedicated copy and skips reconcile when legacy blob deletes fail.Account lifecycle:
accountOperatorOwnedD1Surfacesand export exclusions document the dedicated tables alongside existingsystem:emailexclusions; retention dispositions mark the four tables asalternate_cleanupin step 4a.Reviewed by Cursor Bugbot for commit a389723. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Data Management