Skip to content

[PB-6473]: feat/mail blob pointer table#74

Merged
jzunigax2 merged 10 commits into
masterfrom
feat/mail-blob-pointer-table
Jul 17, 2026
Merged

[PB-6473]: feat/mail blob pointer table#74
jzunigax2 merged 10 commits into
masterfrom
feat/mail-blob-pointer-table

Conversation

@jzunigax2

@jzunigax2 jzunigax2 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Keep track of mail blobs using the new bucket entries table, on ingest event firing we store a pointer to the related network bucket entry so we can later on a delete event decrement size

Summary by CodeRabbit

  • New Features

    • Added tracking of stored message usage and associated storage entries.
    • Added automatic usage release when messages are permanently deleted.
    • Added safeguards to prevent duplicate tracking and maintain consistency during failures.
  • Bug Fixes

    • Improved handling when tracked entries are already missing from storage.
    • Preserved successful message deletion even if usage release encounters an error.
    • Corrected deleted-message entry identifiers for more reliable cleanup.

@jzunigax2 jzunigax2 self-assigned this Jun 25, 2026
@jzunigax2 jzunigax2 added the enhancement New feature or request label Jun 25, 2026
@jzunigax2
jzunigax2 requested a review from xabg2 June 25, 2026 05:16
@jzunigax2
jzunigax2 force-pushed the feat/stalwart-events-webhook branch from 96cf7aa to 2a9c787 Compare June 30, 2026 14:54
Base automatically changed from feat/stalwart-events-webhook to master June 30, 2026 23:22
jzunigax2 added 6 commits July 1, 2026 17:34
- Added `findBucketContextByProviderInternalId` method to `AddressRepository` for resolving user and bucket context.
- Updated `AccountService` to delete provider links during account provisioning failure.
- Implemented `createBucketEntry` method in `BridgeClient` for creating bucket entries in response to Stalwart events.
- Enhanced `StalwartEventsService` to handle batch events and create bucket entries based on resolved account context.
- Added unit tests for the new functionality in `StalwartEventsService` and `AddressRepository` to ensure proper behavior.
- Updated `deleteEmail` method in `EmailService` to handle quota entry release upon email deletion.
- Introduced `releaseQuotaEntry` method to manage quota entries based on email deletion results.
- Modified `deleteEmail` in `MailProvider` to return a result object containing the deleted entry key.
- Enhanced unit tests for `EmailService` and `JmapMailProvider` to cover new deletion behavior and quota management scenarios.
- Integrated `BridgeClient` for handling quota entry deletions in the bridge service.
- Introduced `decodeStalwartIdBig` function to handle Stalwart IDs exceeding JavaScript's safe integer range, allowing for proper decoding of email IDs.
- Updated `decodeStalwartId` to utilize the new decoding function and throw an error for IDs exceeding safe integer limits.
- Enhanced `StalwartEventsAuthGuard` to throw an `UnauthorizedException` for malformed authorization headers lacking a colon separator.
- Added unit tests for the new decoding function and authentication guard to ensure robust error handling and functionality.
- Added migration for creating the `mail_bucket_entries` table to store email usage data.
- Introduced `MailUsageModule` and `MailUsageService` for managing email bucket entries and tracking usage.
- Updated `AddressRepository`, `EmailService`, and `StalwartEventsService` to integrate with the new mail usage functionality.
- Refactored `BridgeClient` methods to accommodate changes in bucket entry management.
- Enhanced models and repositories to support the new mail bucket entry structure.
- Modified `AddressRepository` to include `mailAccountId` in the returned results and updated account structure.
- Refactored `EmailService` to replace `BridgeClient` with `MailUsageService` for managing email quota entries.
- Enhanced unit tests across `AddressRepository`, `EmailService`, and `StalwartEventsService` to reflect changes in account ID handling and usage tracking.
- Introduced new tests for `MailUsageService` and `MailBucketEntryRepository` to ensure proper functionality and error handling.
…nd repositories

- Updated migration to change `mail_account_id` to `mail_address_id` in the `mail_bucket_entries` table.
- Refactored `AddressRepository`, `EmailService`, and `StalwartEventsService` to use `mailAddressId` instead of `mailAccountId`.
- Adjusted related models, repositories, and unit tests to reflect the new naming convention for better clarity and consistency.
- Enhanced tests in `MailUsageService` and `MailBucketEntryRepository` to ensure proper functionality with the updated identifiers.
@jzunigax2
jzunigax2 force-pushed the feat/mail-blob-pointer-table branch from efddac8 to 6692bb4 Compare July 1, 2026 23:40
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds persisted mail bucket-entry tracking, updates BridgeClient entry operations, and routes Stalwart ingest and permanent email deletion through MailUsageService. Account bucket context now includes the mail address ID.

Changes

Mail usage tracking

Layer / File(s) Summary
Mail bucket entry persistence
migrations/..., src/modules/usage/domain/..., src/modules/usage/models/..., src/modules/usage/repositories/...
Adds the mail_bucket_entries schema, domain entity, Sequelize model, repository, duplicate-key mapping, and repository tests.
Usage tracking and release service
src/modules/usage/mail-usage.service.ts, src/modules/usage/mail-usage.module.ts, src/modules/usage/mail-usage.service.spec.ts
Adds idempotent message tracking, persistence rollback, bridge-entry release, 404 handling, and service tests.
Bridge entry API contract
src/modules/infrastructure/bridge/bridge.service.ts, src/modules/infrastructure/bridge/bridge.service.spec.ts
Creates entries using size only and deletes entries by ID while returning a user-space snapshot.
Ingest event tracking integration
src/modules/account/repositories/address.repository.ts, src/modules/stalwart-events/...
Adds mailAddressId to bucket context and routes ingest events to trackStoredMessage.
Email deletion release integration
src/modules/email/..., src/modules/infrastructure/jmap/jmap-mail.provider.ts
Routes quota release through releaseStoredMessage and masks the generated JMAP account component.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StalwartEventsService
  participant MailUsageService
  participant BridgeClient
  participant MailBucketEntryRepository
  StalwartEventsService->>MailUsageService: trackStoredMessage(params)
  MailUsageService->>MailBucketEntryRepository: findByEntryKey(entryKey)
  MailUsageService->>BridgeClient: createBucketEntry(userUuid, bucketId, size)
  MailUsageService->>MailBucketEntryRepository: create(mailAddressId, entryKey, bridgeEntryId, size)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and broadly matches the main change: introducing a new mail blob pointer/tracking table and related plumbing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mail-blob-pointer-table

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jzunigax2
jzunigax2 marked this pull request as ready for review July 13, 2026 22:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/modules/email/email.service.ts (1)

373-377: 🗄️ Data Integrity & Integration | 🔵 Trivial

Provide a durable retry for failed quota release.

releaseStoredMessage runs after permanent deletion, while the surrounding catch suppresses failures. If usage release fails, the message is gone and no later deletion retries the quota update. Confirm that an outbox, reconciliation job, or equivalent retry path exists; otherwise quota can remain overstated.

🤖 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 `@src/modules/email/email.service.ts` around lines 373 - 377, Ensure the
permanent-deletion flow around releaseStoredMessage has a durable retry
mechanism for failed quota release, such as an existing outbox or reconciliation
job. If no equivalent path exists, persist the release operation for later retry
instead of allowing the surrounding suppressed failure to lose the quota update,
while preserving the current deletion behavior.
🤖 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 `@src/modules/infrastructure/jmap/jmap-mail.provider.ts`:
- Around line 845-847: The entry-key construction must preserve complete
Stalwart IDs instead of truncating them with the 32-bit masks in the visible
account/email ID conversion logic. Update the identifiers used by the returned
key to retain full decoded values (or the original ID strings), ensuring IDs
differing by 2^32 produce distinct keys, and add a regression test covering that
case.

In `@src/modules/usage/mail-usage.service.ts`:
- Around line 43-60: Update the persistence error handling around entries.create
in the mail usage flow to roll back the minted bridge bucket entry for every
failure, while preserving the existing duplicate-entry debug behavior. Guard
bridge.deleteBucketEntry so rollback failures are caught and logged with enough
context for reconciliation, then rethrow the original persistence error. Update
mail-usage.service.spec.ts to expect rollback for generic persistence failures
and verify rollback errors are handled.
- Around line 68-93: The release flow in releaseStoredMessage must be idempotent
when BridgeClient.deleteBucketEntry reports a missing bridge entry. Handle the
bridge client's 404/not-found error as successful and continue deleting the
local entry, while still propagating other bridge deletion failures; preserve
the existing missing-local-entry early return and success logging.

---

Nitpick comments:
In `@src/modules/email/email.service.ts`:
- Around line 373-377: Ensure the permanent-deletion flow around
releaseStoredMessage has a durable retry mechanism for failed quota release,
such as an existing outbox or reconciliation job. If no equivalent path exists,
persist the release operation for later retry instead of allowing the
surrounding suppressed failure to lose the quota update, while preserving the
current deletion behavior.
🪄 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

Run ID: 54d91ed6-bec9-4e5b-bed7-3966d50ae1c2

📥 Commits

Reviewing files that changed from the base of the PR and between 9b4db20 and 3696023.

📒 Files selected for processing (19)
  • migrations/20260623120000-create-mail-bucket-entries.js
  • src/modules/account/repositories/address.repository.spec.ts
  • src/modules/account/repositories/address.repository.ts
  • src/modules/email/email.module.ts
  • src/modules/email/email.service.spec.ts
  • src/modules/email/email.service.ts
  • src/modules/infrastructure/bridge/bridge.service.spec.ts
  • src/modules/infrastructure/bridge/bridge.service.ts
  • src/modules/infrastructure/jmap/jmap-mail.provider.ts
  • src/modules/stalwart-events/stalwart-events.module.ts
  • src/modules/stalwart-events/stalwart-events.service.spec.ts
  • src/modules/stalwart-events/stalwart-events.service.ts
  • src/modules/usage/domain/mail-bucket-entry.domain.ts
  • src/modules/usage/mail-usage.module.ts
  • src/modules/usage/mail-usage.service.spec.ts
  • src/modules/usage/mail-usage.service.ts
  • src/modules/usage/models/mail-bucket-entry.model.ts
  • src/modules/usage/repositories/mail-bucket-entry.repository.spec.ts
  • src/modules/usage/repositories/mail-bucket-entry.repository.ts

Comment on lines +845 to 847
const numericAccountId = decodeStalwartIdBig(accountId) & 0xffffffffn;
const documentId = decodeStalwartIdBig(emailId) & 0xffffffffn;
return `${numericAccountId}:${documentId}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
node <<'NODE'
const mask = (value) => value & 0xffffffffn;
const a = 1n;
const b = (1n << 32n) + 1n;
if (mask(a) !== mask(b)) process.exit(1);
console.log(`${mask(a)} === ${mask(b)}`);
NODE

Repository: internxt/mail-server

Length of output: 166


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\n## provider outline\n'
ast-grep outline src/modules/infrastructure/jmap/jmap-mail.provider.ts --view expanded | sed -n '1,220p'

printf '\n## usage service outline\n'
ast-grep outline src/modules/usage/mail-usage.service.ts --view expanded | sed -n '1,240p'

printf '\n## entryKey references\n'
rg -n "entryKey|decodeStalwartIdBig|0xffffffffn|release lookup|duplicate" src/modules -S

Repository: internxt/mail-server

Length of output: 10366


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\n## provider slice around target lines\n'
sed -n '820,860p' src/modules/infrastructure/jmap/jmap-mail.provider.ts | cat -n

printf '\n## usage service slice around entryKey usage\n'
sed -n '1,260p' src/modules/usage/mail-usage.service.ts | cat -n

Repository: internxt/mail-server

Length of output: 4734


Do not truncate Stalwart IDs to 32 bits. & 0xffffffffn makes distinct large IDs collide in entryKey, which this path uses for duplicate detection and release. That can skip tracking or release the wrong stored message. Keep the full decoded IDs, or use the original ID strings, and add a regression test for values that differ by 2^32.

🤖 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 `@src/modules/infrastructure/jmap/jmap-mail.provider.ts` around lines 845 -
847, The entry-key construction must preserve complete Stalwart IDs instead of
truncating them with the 32-bit masks in the visible account/email ID conversion
logic. Update the identifiers used by the returned key to retain full decoded
values (or the original ID strings), ensuring IDs differing by 2^32 produce
distinct keys, and add a regression test covering that case.

Comment thread src/modules/usage/mail-usage.service.ts
Comment thread src/modules/usage/mail-usage.service.ts
const { statusCode, body } = await this.httpClient.request({
method: 'DELETE',
path: `${this.basePath}/v2/gateway/users/${encodeURIComponent(userUuid)}/buckets/${encodeURIComponent(bucketId)}/entries/${encodeURIComponent(key)}`,
path: `${this.basePath}/v2/gateway/users/${encodeURIComponent(userUuid)}/buckets/${encodeURIComponent(bucketId)}/entries/${encodeURIComponent(entryId)}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about extracting the encodeURIComponent(userUuid) / encodeURIComponent(bucketId) / encodeURIComponent(entryId) to a constant?

Something like encodedUserUuid = ... and so on.


describe('handleBatch', () => {
it('when an ingest event resolves to a bucket, then creates a bucket entry keyed by accountId:documentId', async () => {
it('when an ingest event resolves to a bucket, then tracks the stored message keyed by accountId:documentId', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use test instead

});

describe('trackStoredMessage', () => {
it('when the entry is new, then mints a bridge entry and persists the pointer', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, use test instead

Comment thread src/modules/usage/mail-usage.service.ts
…ess rollback on errors

- Updated all test cases in the `bridge.service.spec.ts`, `stalwart-events.service.spec.ts`, `mail-usage.service.spec.ts`, and `mail-bucket-entry.repository.spec.ts` files to use 'test' instead of 'it' for improved readability and uniformity across the test suite.
- Added error handling and rollback logic in `mail-usage.service.ts` to manage failures during bucket entry deletion, enhancing robustness.
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
migrations/20260717233856-create-mail-bucket-entries.js (1)

7-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Wrap migration operations in a transaction.

When a migration performs multiple operations sequentially (such as creating a table and then adding an index), it is highly recommended to wrap them in a transaction. This ensures that if a subsequent operation fails, the entire migration rolls back cleanly, avoiding a partially applied database state that could break future migration runs.

Note: DDL transactions are supported natively in PostgreSQL and SQLite, though not in MySQL.

♻️ Proposed refactor
-  async up(queryInterface, Sequelize) {
-    await queryInterface.createTable(TABLE_NAME, {
-      id: {
-        type: Sequelize.UUID,
-        defaultValue: Sequelize.literal('gen_random_uuid()'), // using suggested fix
-        primaryKey: true,
-        allowNull: false,
-      },
-      mail_address_id: {
-        type: Sequelize.UUID,
-        allowNull: false,
-        references: { model: 'mail_addresses', key: 'id' },
-        onUpdate: 'CASCADE',
-        onDelete: 'CASCADE',
-      },
-      entry_key: {
-        type: Sequelize.STRING(255),
-        allowNull: false,
-        unique: true,
-      },
-      bridge_entry_id: {
-        type: Sequelize.STRING(24),
-        allowNull: false,
-      },
-      size: {
-        type: Sequelize.BIGINT,
-        allowNull: false,
-      },
-      created_at: {
-        type: Sequelize.DATE,
-        allowNull: false,
-        defaultValue: Sequelize.fn('now'),
-      },
-      updated_at: {
-        type: Sequelize.DATE,
-        allowNull: false,
-        defaultValue: Sequelize.fn('now'),
-      },
-    });
-
-    await queryInterface.addIndex(TABLE_NAME, ['mail_address_id']);
-  },
+  async up(queryInterface, Sequelize) {
+    await queryInterface.sequelize.transaction(async (transaction) => {
+      await queryInterface.createTable(TABLE_NAME, {
+        id: {
+          type: Sequelize.UUID,
+          defaultValue: Sequelize.literal('gen_random_uuid()'),
+          primaryKey: true,
+          allowNull: false,
+        },
+        mail_address_id: {
+          type: Sequelize.UUID,
+          allowNull: false,
+          references: { model: 'mail_addresses', key: 'id' },
+          onUpdate: 'CASCADE',
+          onDelete: 'CASCADE',
+        },
+        entry_key: {
+          type: Sequelize.STRING(255),
+          allowNull: false,
+          unique: true,
+        },
+        bridge_entry_id: {
+          type: Sequelize.STRING(24),
+          allowNull: false,
+        },
+        size: {
+          type: Sequelize.BIGINT,
+          allowNull: false,
+        },
+        created_at: {
+          type: Sequelize.DATE,
+          allowNull: false,
+          defaultValue: Sequelize.fn('now'),
+        },
+        updated_at: {
+          type: Sequelize.DATE,
+          allowNull: false,
+          defaultValue: Sequelize.fn('now'),
+        },
+      }, { transaction });
+
+      await queryInterface.addIndex(TABLE_NAME, ['mail_address_id'], { transaction });
+    });
+  },
🤖 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 `@migrations/20260717233856-create-mail-bucket-entries.js` around lines 7 - 48,
Wrap the sequential operations in the up migration with a transaction, passing
the transaction option to both createTable and addIndex and committing only
after both succeed. Roll back the transaction on failure while preserving the
existing table schema and index definitions in up.
🤖 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.

Nitpick comments:
In `@migrations/20260717233856-create-mail-bucket-entries.js`:
- Around line 7-48: Wrap the sequential operations in the up migration with a
transaction, passing the transaction option to both createTable and addIndex and
committing only after both succeed. Roll back the transaction on failure while
preserving the existing table schema and index definitions in up.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b364ea74-9273-4188-af86-16ee7a78fb83

📥 Commits

Reviewing files that changed from the base of the PR and between b1c4902 and 0872edf.

📒 Files selected for processing (1)
  • migrations/20260717233856-create-mail-bucket-entries.js

@jzunigax2
jzunigax2 merged commit 6a1bf01 into master Jul 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants