Skip to content

feat: implement MTA hooks for quota enforcement#82

Open
jzunigax2 wants to merge 1 commit into
masterfrom
feat/mta-hooks-read-only-quota
Open

feat: implement MTA hooks for quota enforcement#82
jzunigax2 wants to merge 1 commit into
masterfrom
feat/mta-hooks-read-only-quota

Conversation

@jzunigax2

@jzunigax2 jzunigax2 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
  • Added MTA hooks module with authentication guard, controller, and service.
  • Implemented handleRcpt method in MtaHooksService to manage recipient quota checks.
  • Introduced getUserUsage method in BridgeClient for fetching user storage usage.
  • Updated .env.template to include MTA hooks credentials.
  • Added unit tests for MTA hooks functionality and authentication guard to ensure reliability.

Summary by CodeRabbit

  • New Features

    • Added support for MTA hook handling on recipient checks.
    • Incoming requests can now be authenticated with Basic Auth credentials.
    • Recipient quota usage is now checked before accepting mail, with a clear rejection when over quota.
  • Bug Fixes

    • Improved handling of missing or invalid recipient details by allowing mail to continue when usage can’t be determined.
    • Added safeguards for uppercase recipient addresses and multi-recipient requests.

- Added MTA hooks module with authentication guard, controller, and service.
- Implemented `handleRcpt` method in `MtaHooksService` to manage recipient quota checks.
- Introduced `getUserUsage` method in `BridgeClient` for fetching user storage usage.
- Updated `.env.template` to include MTA hooks credentials.
- Added unit tests for MTA hooks functionality and authentication guard to ensure reliability.
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@jzunigax2 jzunigax2 self-assigned this Jul 6, 2026
@jzunigax2 jzunigax2 marked this pull request as ready for review July 9, 2026 01:01
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an MTA Hooks feature: a NestJS module, controller, and service handling Stalwart's RCPT hook to enforce mailbox quota using Bridge usage data, protected by a Basic-auth guard. Adds protocol types, config/env variables, a Bridge client getUserUsage method, and accompanying tests.

Changes

MTA Hooks feature

Layer / File(s) Summary
Protocol types and config
src/modules/mta-hooks/mta-hooks.types.ts, src/config/configuration.ts, .env.template
Defines MtaHookStage, MtaHookAddress, MtaHookEnvelope, MtaHookRequest, MtaHookAction, MtaHookSmtpResponse, MtaHookResponse types; adds mtaHooks config from MTA_HOOKS_USERNAME/MTA_HOOKS_SECRET env vars.
Bridge usage lookup
src/modules/infrastructure/bridge/bridge.service.ts, bridge.service.spec.ts
Adds getUserUsage(userUuid) to BridgeClient, signing a gateway JWT and GETting the usage endpoint, throwing BridgeApiError on non-200, with tests for success/failure.
Basic auth guard
src/modules/mta-hooks/mta-hooks-auth.guard.ts, mta-hooks-auth.guard.spec.ts
Adds MtaHooksAuthGuard validating Authorization: Basic header against configured credentials via constant-time comparison, with tests for valid/invalid/malformed cases.
Quota enforcement service
src/modules/mta-hooks/mta-hooks.service.ts, mta-hooks.service.spec.ts
Adds MtaHooksService.handleRcpt resolving recipient, parsing declared SIZE, checking quota via AccountService/BridgeClient, rejecting on overage and failing open on errors, with extensive tests.
Controller and module wiring
src/modules/mta-hooks/mta-hooks.controller.ts, mta-hooks.module.ts, src/app.module.ts
Adds MtaHooksController exposing POST /mta-hooks/rcpt guarded by MtaHooksAuthGuard, MtaHooksModule wiring dependencies, and registers the module in AppModule.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Stalwart
  participant MtaHooksController
  participant MtaHooksAuthGuard
  participant MtaHooksService
  participant AccountService
  participant BridgeClient

  Stalwart->>MtaHooksController: POST /mta-hooks/rcpt (MtaHookRequest)
  MtaHooksController->>MtaHooksAuthGuard: canActivate(request)
  MtaHooksAuthGuard-->>MtaHooksController: allowed or UnauthorizedException
  MtaHooksController->>MtaHooksService: handleRcpt(request)
  MtaHooksService->>AccountService: findUserIdByAddress(recipient)
  AccountService-->>MtaHooksService: userUuid
  MtaHooksService->>BridgeClient: getUserUsage(userUuid)
  BridgeClient-->>MtaHooksService: maxSpaceBytes, totalUsedSpaceBytes
  MtaHooksService-->>MtaHooksController: accept or reject response
  MtaHooksController-->>Stalwart: MtaHookResponse
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding MTA hooks to enforce quota checks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/mta-hooks-read-only-quota

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.

@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: 2

🧹 Nitpick comments (2)
.env.template (1)

33-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

dotenv-linter flags key ordering.

MTA_HOOKS_SECRET should appear before MTA_HOOKS_USERNAME per the linter's alphabetical ordering rule.

🔧 Fix key ordering
 # MTA hooks (RCPT)
-MTA_HOOKS_USERNAME=stalwart
 MTA_HOOKS_SECRET=
+MTA_HOOKS_USERNAME=stalwart
🤖 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 @.env.template around lines 33 - 36, The dotenv linter is flagging the MTA
hooks entries for incorrect alphabetical ordering. Reorder the variables in the
.env.template section so MTA_HOOKS_SECRET appears before MTA_HOOKS_USERNAME,
keeping the existing values unchanged and preserving the surrounding comment and
section structure.

Source: Linters/SAST tools

src/modules/mta-hooks/mta-hooks-auth.guard.spec.ts (1)

65-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test case for empty-secret credentials.

This would guard against the empty-secret bypass (see comment on the guard). When the configured secret is non-empty, sending stalwart: (empty password) should reject.

♻️ Suggested additional test
+  it('when the secret is empty, then throws Unauthorized', () => {
+    const context = contextWithAuth(basic(username, ''));
+
+    expect(() => guard.canActivate(context)).toThrow(UnauthorizedException);
+  });
🤖 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/mta-hooks/mta-hooks-auth.guard.spec.ts` around lines 65 - 71, Add
a spec in mta-hooks-auth.guard.spec for the empty-secret bypass case by
extending the existing Basic auth parsing tests around guard.canActivate and
contextWithAuth. Verify that when the configured secret is non-empty, a Basic
credential decoding to a username with an empty password such as stalwart: is
rejected with UnauthorizedException, alongside the existing no-colon test. Use
the guard’s credential validation path in AuthGuard to locate the behavior being
exercised.
🤖 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/config/configuration.ts`:
- Around line 57-60: The MTA hooks Basic auth check in configuration handling
currently allows an empty secret, which can let `stalwart:` pass as valid
credentials. Update the logic around `mtaHooks` in `configuration.ts` and the
related startup/request guard so `MTA_HOOKS_SECRET` must be non-empty: fail
startup or reject access when the secret resolves to an empty string, and ensure
the check uses the `MTA_HOOKS_SECRET`/`mtaHooks` values consistently.

In `@src/modules/mta-hooks/mta-hooks-auth.guard.ts`:
- Around line 16-20: The auth guard in MTAHooksAuthGuard is accepting an empty
secret because ConfigService.getOrThrow only rejects undefined, not ''. Update
the constructor to validate that both mtaHooks.username and mtaHooks.secret are
non-empty strings before storing them, and fail fast if the secret is missing or
blank. Use the existing MTAHooksAuthGuard constructor and
expectedSecret/expectedUsername fields to locate the change, and keep the
validation aligned with the mtaHooks configuration contract.

---

Nitpick comments:
In @.env.template:
- Around line 33-36: The dotenv linter is flagging the MTA hooks entries for
incorrect alphabetical ordering. Reorder the variables in the .env.template
section so MTA_HOOKS_SECRET appears before MTA_HOOKS_USERNAME, keeping the
existing values unchanged and preserving the surrounding comment and section
structure.

In `@src/modules/mta-hooks/mta-hooks-auth.guard.spec.ts`:
- Around line 65-71: Add a spec in mta-hooks-auth.guard.spec for the
empty-secret bypass case by extending the existing Basic auth parsing tests
around guard.canActivate and contextWithAuth. Verify that when the configured
secret is non-empty, a Basic credential decoding to a username with an empty
password such as stalwart: is rejected with UnauthorizedException, alongside the
existing no-colon test. Use the guard’s credential validation path in AuthGuard
to locate the behavior being exercised.
🪄 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: 26106eeb-7d02-47e8-93d7-20a9ea34f049

📥 Commits

Reviewing files that changed from the base of the PR and between 5c57964 and cede383.

📒 Files selected for processing (12)
  • .env.template
  • src/app.module.ts
  • src/config/configuration.ts
  • src/modules/infrastructure/bridge/bridge.service.spec.ts
  • src/modules/infrastructure/bridge/bridge.service.ts
  • src/modules/mta-hooks/mta-hooks-auth.guard.spec.ts
  • src/modules/mta-hooks/mta-hooks-auth.guard.ts
  • src/modules/mta-hooks/mta-hooks.controller.ts
  • src/modules/mta-hooks/mta-hooks.module.ts
  • src/modules/mta-hooks/mta-hooks.service.spec.ts
  • src/modules/mta-hooks/mta-hooks.service.ts
  • src/modules/mta-hooks/mta-hooks.types.ts

Comment on lines +57 to +60
mtaHooks: {
username: process.env.MTA_HOOKS_USERNAME ?? 'stalwart',
secret: process.env.MTA_HOOKS_SECRET ?? '',
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the auth guard rejects empty secrets
rg -n 'secret' src/modules/mta-hooks/mta-hooks-auth.guard.ts -C3

Repository: internxt/mail-server

Length of output: 875


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== configuration.ts ==\n'
cat -n src/config/configuration.ts | sed -n '45,75p'

printf '\n== mta-hooks auth guard ==\n'
cat -n src/modules/mta-hooks/mta-hooks-auth.guard.ts | sed -n '1,120p'

printf '\n== search for mtaHooks.secret validation/usages ==\n'
rg -n "mtaHooks\.secret|MTA_HOOKS_SECRET|mtaHooks" src -C 2

Repository: internxt/mail-server

Length of output: 5316


Reject empty MTA_HOOKS_SECRET values
If MTA_HOOKS_SECRET is unset, the guard compares against '', so a Basic auth value of stalwart: will satisfy the check and expose the MTA hooks endpoint. Fail startup or reject requests when the secret is empty.

🤖 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/config/configuration.ts` around lines 57 - 60, The MTA hooks Basic auth
check in configuration handling currently allows an empty secret, which can let
`stalwart:` pass as valid credentials. Update the logic around `mtaHooks` in
`configuration.ts` and the related startup/request guard so `MTA_HOOKS_SECRET`
must be non-empty: fail startup or reject access when the secret resolves to an
empty string, and ensure the check uses the `MTA_HOOKS_SECRET`/`mtaHooks` values
consistently.

Comment on lines +16 to +20
constructor(configService: ConfigService) {
this.expectedUsername =
configService.getOrThrow<string>('mtaHooks.username');
this.expectedSecret = configService.getOrThrow<string>('mtaHooks.secret');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Empty secret default enables authentication bypass when MTA_HOOKS_SECRET is unset.

The config contract (src/config/configuration.ts:59) defaults mtaHooks.secret to '':

secret: process.env.MTA_HOOKS_SECRET ?? '',

getOrThrow only throws on undefined, not empty strings, so the guard silently accepts '' as the expected secret. An attacker can then authenticate with Basic c3RhbHdhcnQ6 (decodes to stalwart:), since safeEqual('', '') passes the length check and timingSafeEqual returns true for two zero-length buffers.

Validate non-empty at construction time:

🔒️ Proposed fix
  constructor(configService: ConfigService) {
    this.expectedUsername =
      configService.getOrThrow<string>('mtaHooks.username');
    this.expectedSecret = configService.getOrThrow<string>('mtaHooks.secret');
+   if (!this.expectedSecret) {
+     throw new Error(
+       'mtaHooks.secret must be configured with a non-empty value',
+     );
+   }
  }
🤖 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/mta-hooks/mta-hooks-auth.guard.ts` around lines 16 - 20, The auth
guard in MTAHooksAuthGuard is accepting an empty secret because
ConfigService.getOrThrow only rejects undefined, not ''. Update the constructor
to validate that both mtaHooks.username and mtaHooks.secret are non-empty
strings before storing them, and fail fast if the secret is missing or blank.
Use the existing MTAHooksAuthGuard constructor and
expectedSecret/expectedUsername fields to locate the change, and keep the
validation aligned with the mtaHooks configuration contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant