Repository navigation
Rate-limit SMTP messages, cap subjects at 998 characters and fix the suppressed details - #130
Merged
Merged
Conversation
…s body At a phishing suspension the capture reads one stored email per sender and subject among the team's latest. It took the newest row of each, so a newer row without a body (one the retention purge emptied, or one whose body was never stored) hid an older row of the same campaign that still had it, and the capture lost the campaign's link domains, phone numbers and text hash. Each sender and subject now reads its newest email that still has a body, and falls back to its newest only when none of its scanned rows has one. The scan size, and the cap on distinct senders and subjects counted newest first, stay as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The suppressed details drawer on the email page looked its suppression up by the first to address only, through a search on the stored address and a case-sensitive comparison, so a display name or a capital letter missed the entry and other suppressed recipients never showed. A new emails.suppressions.forEmail query matches every to, cc and bcc recipient of the email by the same normalized hashes the send path checks, within the caller's team, and the drawer lists each match with its origin, date and its own Remove action. An entry whose address was erased still matches by hash and shows no address. The drawer's note named a 422 suppressed_recipient that does not exist and said every entry blocks every send. It now says an unsubscribe only blocks topic sends and broadcasts, and that a send left with no to recipient is refused with 422 all_recipients_suppressed. A send refused at accept because every to recipient was suppressed never had its body stored, so the content panel says that instead of claiming the body was purged after the retention window. The line keys on the email's suppressed event coming from the API rather than the worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /emails, each item of POST /emails/batch and broadcast create and update took a subject of any length, held back only by the request body limit, while templates and the dashboard's broadcast editor already stopped at 998 characters, the RFC 5322 line limit, each with its own literal. MAX_SUBJECT_LENGTH in core now holds that limit and every one of them uses it: the API's request schemas, so the OpenAPI document and the MCP tools built on them carry maxLength; the dashboard's broadcast and template routers, including the broadcast test send, which had no cap; and the SMTP relay, which answers 554 as it does for its other Subject checks. Inbox replies, forwards, drafts and the inbox composer keep the 2000 that Resend documents for them. The API reference lists the limit among the Resend deltas, in en and pt-BR, since Resend documents none for these endpoints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…with the API The relay only capped connections, so an authenticated session could submit messages with no per-minute bound. Each MAIL FROM now counts against the key's minute bucket in api_rate_limits, the one its API requests spend, and against a per-team counter in the relay process, with the API's defaults (API_RATE_LIMIT_PER_MINUTE per key, 3,000 per team). Past either limit the relay answers 451 4.7.1 with the seconds left in the minute, before any of the message is received. A transaction opened after RSET counts again, and a session that has not authenticated never reaches the check. The fixed-window counter and the key bucket upsert move from the API into packages/core so both surfaces share them; the API's limits answer exactly as before. The per-key default becomes one exported constant that the env schema, the API, its MCP gate and the relay read. The self-hosting docs, in English and Brazilian Portuguese, describe the shared budget and the 451 4.7.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The migration copies Resend's bounce, complaint and manual suppressions, and MillionSend refuses a send whose every `to` recipient is suppressed with 422 all_recipients_suppressed, where Resend accepts it and records the email as suppressed. The API reference listed this as a deliberate difference, but the migration guide and the agent prompt did not, so code that kept mailing those addresses would meet an unexpected error after cutover. The guide's list of differences gains that case, with what to do (treat the error as "not sent" rather than retry it) and how one such item fails a strict batch whole; the agent prompt says the same, in both languages. The pt-BR API reference also gains the sentence the English one already had on which suppression origins count for transactional and topic sends. The suppressions page the new item links to said the SMTP relay answers that 422 too; it answers 550 with the same message, and now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gaps found while reviewing #129, which records sends refused because every recipient is suppressed. Each commit explains its own change; the list below is what clients and operators will notice.
Behaviour changes (release notes)
MAIL FROMcounts against the API key's per-minute budget, the same one the key's API requests spend (API_RATE_LIMIT_PER_MINUTE, default 600). It also counts against 3,000 a minute per team in the relay. Past either limit the relay answers451 4.7.1with the seconds left, before any of the message is sent, so the client keeps the message and retries.POST /emails, eachPOST /emails/batchitem, broadcast create and update, and the dashboard's broadcast test send; templates and the dashboard's broadcast editor already had it. A longer subject gets422 validation_error, and the SMTP relay answers554. Inbox replies, forwards and drafts keep their 2,000. Resend documents no limit for these endpoints, so the limit is listed among the Resend deltas.Dashboard
422 suppressed_recipientthat doesn't exist. It now gives the real rule and the real422 all_recipients_suppressed.Trust & safety
At a suspension, the campaign fingerprint capture now reads, for each sender and subject, the newest email that still has its body. A newer row without a body no longer hides an older one's fingerprints.
Docs
422 all_recipients_suppressed; Resend accepts it and marks the emailsuppressed.550there, not422.All of it is in en and pt-BR.
Checks
pnpm lint,pnpm typecheck,turbo run test(4,707 tests) andpnpm db:generatepass on this branch. Every behaviour fix has a test that fails without it.Follow-ups (not in this PR)
🤖 Generated with Claude Code