Skip to content

fix(emails): record the account on rejected sends (chat#1889 row 27) - #790

Open
sweetmantech wants to merge 1 commit into
mainfrom
fix/log-account-on-rejected-sends
Open

fix(emails): record the account on rejected sends (chat#1889 row 27)#790
sweetmantech wants to merge 1 commit into
mainfrom
fix/log-account-on-rejected-sends

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Matrix row 27 in chat#1889.

Why

sendEmailHandler.ts L59 set attempt = { status: "rejected" } with no account, so every account-scoped email_send_log audit under-reported.

Concretely, in the 2026-07-27 scheduled-run incident: 8 POST /api/emails attempts, 2 sent and 6 rejected. An account-scoped query returned 2 of 8 — the six rejections were findable only by filtering raw_body on the recipient. The recoup-internal-task-email-audit skill is account-scoped, so it under-reports the same way. This is why the incident went unnoticed.

What changed

validateSendEmailBody's error branch now carries accountId wherever one can be resolved, and the handler records it:

Rejection path Account recorded
Schema / body invalid (400) — (nothing resolved yet)
Auth failed (401) the account the body claimed (account_id, already schema-validated as a UUID)
No recipient on file (400) the authenticated account
Recipient restriction (403) the authenticated account

The auth-failure row is the one that matters: the six rejections in the incident were validateAuthContext failures after the sandbox credential expired ~1h from kickoff, so an expired credential is precisely how a legitimate account's sends vanish from the audit.

Deliberate trade-off, worth a reviewer's eye

On the auth-failed path the recorded id is the account the request claimed, not one we verified. That is the only thing knowable there, and attribution is the point of the log — but it does mean a caller could write rows referencing an account they don't hold. I judged that acceptable because the row is an internal audit record, its status is rejected (so it is never evidence of a successful action), and raw_body is stored alongside. It is documented on ValidateSendEmailResult as "the best-known account, not proof of authorization".

If you'd rather not record unverified ids, the alternative is to carry only the authenticated cases — but then the incident's six rejections still would not be attributable, which defeats the row. Happy to switch on your call.

Verification

TDD, red → green:

# RED
FAIL lib/emails/__tests__/sendEmailHandler.test.ts (2 failed)
  > records the account on a rejected attempt when validation resolved one
  > still logs a rejected attempt when no account could be resolved

# GREEN — full api suite
pnpm exec vitest run
  Test Files  784 passed (784)
       Tests  4250 passed (4250)

Three tests: the handler forwards a resolved account, the handler still logs when none was resolved (so the fix can't regress into dropping rejections entirely), and validateSendEmailBody surfaces the authenticated account on the 403 path.

pnpm exec tsc --noEmit → 236, identical to main's baseline, none in the touched files. eslint clean.

No docs change: POST /api/emails's request/response contract is unchanged — this only affects what we write to email_send_log.

Preview verification to follow on this PR.

Tracked in chat#1889 (matrix row 27).


Summary by cubic

Record the best-known account on rejected email sends so account-scoped email_send_log audits include failures, addressing chat#1889 (row 26). This restores visibility for rejections, including those caused by expired credentials.

  • Bug Fixes
    • validateSendEmailBody now returns accountId on error:
      • 401 auth failed: uses body account_id (schema-validated UUID).
      • 400 no recipient / 403 recipient restriction: uses authenticated account.
      • 400 schema/body invalid: no account.
    • sendEmailHandler records accountId for rejected attempts.
    • Added tests for both resolved and unresolved account cases.
    • Note: On 401, the recorded account is the claimed one; for attribution only, not proof of authorization.

Written for commit 4e2de5d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Email rejection records now retain the associated account information.
    • Account details are preserved across authentication, address, and recipient validation failures, improving auditability and troubleshooting.

sendEmailHandler set attempt = { status: 'rejected' } with no account, so
every account-scoped email_send_log audit under-reported. In the
2026-07-27 scheduled-run incident an account-scoped query returned 2 of 8
attempts; the six rejections were findable only by filtering raw_body on
the recipient -- and recoup-internal-task-email-audit is account-scoped,
so it under-reports the same way.

validateSendEmailBody now returns the best-known account on its error
branch: the authenticated account where auth succeeded and the request
was rejected later, and the account the body claimed when auth itself
failed (the expired-credential case that motivated this). Documented as
attribution, never as evidence of authorization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 28, 2026 7:11pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Rejected email validation outcomes now preserve accountId through the validation result and include it in the handler’s logged email attempt.

Changes

Email rejection context

Layer / File(s) Summary
Propagate account context to rejected attempts
lib/emails/validateSendEmailBody.ts, lib/emails/sendEmailHandler.ts
The validation result supports optional accountId values on errors, failure branches populate that context, and rejected email attempts log it alongside the status.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Poem

A rejected mail now leaves a trace,
With account context in its place.
Validation passes the clue,
Logging records the account too—
Audit trails grow neat and true.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
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.
Solid & Clean Code ✅ Passed The change stays narrowly scoped: one handler and one validator, no new mismatched functions, shallow nesting, and no notable duplication or over-abstraction.
✨ 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 fix/log-account-on-rejected-sends

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.

@sweetmantech sweetmantech changed the title fix(emails): record the account on rejected sends (chat#1889 row 26) fix(emails): record the account on rejected sends (chat#1889 row 27) Jul 28, 2026

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

🤖 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 `@lib/emails/validateSendEmailBody.ts`:
- Line 95: Reduce oversized domain functions by extracting cohesive helpers from
validateSendEmailBody for parsing, authentication, recipient resolution, and
recipient validation, while preserving existing behavior; also extract
send-result or audit-attempt construction from sendEmailHandler. Apply the
changes in lib/emails/validateSendEmailBody.ts at lines 95-95 and
lib/emails/sendEmailHandler.ts at lines 59-63, keeping both domain functions
under the repository’s size limits.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ff98d50-31f7-4705-b0a5-f6e7859e7851

📥 Commits

Reviewing files that changed from the base of the PR and between c0a7c7d and 4e2de5d.

⛔ Files ignored due to path filters (2)
  • lib/emails/__tests__/sendEmailHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/emails/__tests__/validateSendEmailBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/emails/sendEmailHandler.ts
  • lib/emails/validateSendEmailBody.ts

const authContext = await validateAuthContext(request, { accountId: result.data.account_id });
if (authContext instanceof NextResponse) {
return { rawBody, error: authContext };
return { rawBody, accountId: result.data.account_id, error: authContext };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Reduce oversized domain functions.

Both changed areas are inside functions that exceed the repository’s function-size limits:

  • lib/emails/validateSendEmailBody.ts#L95-L95,105-105,121-121: extract parsing, authentication, recipient resolution, and recipient checks.
  • lib/emails/sendEmailHandler.ts#L59-L63: extract send-result or audit-attempt construction.

As per coding guidelines, functions longer than 20 lines must be flagged. As per path instructions, domain functions must remain under 50 lines.

📍 Affects 2 files
  • lib/emails/validateSendEmailBody.ts#L95-L95 (this comment)
  • lib/emails/sendEmailHandler.ts#L59-L63
🤖 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 `@lib/emails/validateSendEmailBody.ts` at line 95, Reduce oversized domain
functions by extracting cohesive helpers from validateSendEmailBody for parsing,
authentication, recipient resolution, and recipient validation, while preserving
existing behavior; also extract send-result or audit-attempt construction from
sendEmailHandler. Apply the changes in lib/emails/validateSendEmailBody.ts at
lines 95-95 and lib/emails/sendEmailHandler.ts at lines 59-63, keeping both
domain functions under the repository’s size limits.

Sources: Coding guidelines, Path instructions

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 4 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant Client
    participant Handler as sendEmailHandler
    participant Validator as validateSendEmailBody
    participant Auth as validateAuthContext
    participant RecipientCheck as assertRecipientsAllowed
    participant Logger as logEmailAttempt

    Note over Client,Logger: POST /api/emails — Rejection paths (NEW accountId flow)

    Client->>Handler: POST /api/emails (body with account_id)
    Handler->>Validator: validateSendEmailBody(request)

    alt Body schema invalid (400)
        Validator-->>Handler: { rawBody, error, accountId: undefined }
        Note over Validator: No account resolved
    else Schema valid
        Validator->>Auth: validateAuthContext(request, { accountId: body.account_id })
        alt Auth fails (401)
            Auth-->>Validator: NextResponse error
            Validator-->>Handler: { rawBody, error, accountId: body.account_id }
            Note over Validator: NEW: use body's claimed account_id
        else Auth passes
            Auth-->>Validator: authContext
            Validator->>RecipientCheck: assertRecipientsAllowed(authenticated account)
            alt No recipients (400) or restricted (403)
                RecipientCheck-->>Validator: { allowed: false }
                Validator-->>Handler: { rawBody, error, accountId: authContext.accountId }
            else Allowed
                Validator-->>Handler: { rawBody, data }
            end
        end
    end

    alt Error path (rejected)
        Handler->>Handler: attempt = { status: "rejected", accountId: validated.accountId }
        Note over Handler: NEW: carry accountId from validation
        Handler->>Logger: logEmailAttempt({ rawBody, ...attempt })
    else Success path (unchanged)
        Handler->>Handler: attempt = { status: "sent", ... }
        Handler->>Logger: logEmailAttempt({ rawBody, ...attempt })
        Handler-->>Client: 200
    end
Loading

Auto-approved: Bug fix for email audit logging: records accountId on rejected sends so account-scoped queries include failures. Change is bounded (internal audit only), clearly beneficial, and the trade-off for auth failures is documented and acceptable.

Re-trigger cubic

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