{{ message }}
Repository navigation
fix(messaging): keep a single SMTP recipient in the To header - #13534
Merged
Merged
Conversation
Every SMTP send moved its recipients into BCC and emptied the To list. The default recipient the comment promised was dropped when the messaging Email message stopped taking one, so nothing was left to occupy the field, and the renderer omits a header whose value renders empty. Mail arrived with no To at all, which clients show as a missing recipient and some MTAs score as spam. Hiding recipients from each other only matters when there is more than one, so restrict the rewrite to batches. A lone recipient keeps a real To header, and multi-recipient sends still disclose nothing. No placeholder is put back in To: the SMTP envelope unions to, cc and bcc into the forward paths, so any address there would receive a copy of every batch and be counted as delivered.
Contributor
✨ Benchmark resultsComparing
Per-scenario breakdown & investigation detailsMetrics below reflect the current branch (after). Δ P95 compares against the base.
Top API waits (after) |
A target created automatically alongside the user carries no provider id, and the worker then delivers it through whichever enabled email provider it finds first - which other tests in this suite also create. Create the target bound to the SMTP provider, on its own address so it does not collide with the automatic one.
The e2e mail container runs with MAILDEV_INCOMING_USER and _PASS set, so a provider without credentials is refused and the message ends up failed.
…the message Batch size decided whether recipients stayed visible, but it is a transport artifact: array_chunk() splits on the adapter's per-request limit, recipients are grouped per provider, and retrySend() narrows the batch to whoever is still failing. A topic batch that collapsed to one recipient therefore wrote that address into the To header of a message the cc and bcc addresses also read, and a 1001-subscriber topic hid 1000 subscribers while disclosing the last one. Gate on whether anyone else is on the envelope instead. A lone recipient with no cc or bcc keeps a visible To header; every other shape moves to BCC as before. This also drops the array_diff(), which only ever subtracted the message's explicit bcc list and so left recipients hidden by this same branch exposed when a retry narrowed the batch to one of them.
The suite asserted a visible To header for a lone recipient, then re-sent to a topic whose only subscriber was also on the message's bcc list. That second send produced the same headers before and after the fix, so it pinned nothing, and the bcc assertion could not fail at all: bcc travels in the SMTP envelope and is never written as a header. Send three messages instead, one per shape the worker distinguishes: a lone recipient keeps To, a single subscriber sharing the message with a cc recipient does not, and several subscribers do not. The middle case fails without the worker change. Read the SMTP credentials from _APP_SMTP_* and delete the provider afterwards, as testSendEmailEncryptedAttachment does, and drop that test's comment about SMTP carrying no To header, which no longer holds for its single recipient.
ArnabChatterjee20k
approved these changes
Sep 21, 2026
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.

Closes #12365
Closes #10474
What does this PR do?
An SMTP send moved every recipient into BCC and left
Toempty, so a message to a single recipient arrived with noToheader at all and clients showed no recipient (#12365). The BCC move itself is deliberate — a batch is one MIME message, and topic subscribers must not see each other (#10474) — but it was applied unconditionally, including when there was nobody to hide the recipient from.A recipient now keeps a visible
Toheader when, and only when, no other address shares the message. Every other shape moves to BCC exactly as before.The condition is on the message's addressing rather than on the size of the batch. Batch size is a transport artifact:
array_chunk()splits on the adapter's per-request limit, recipients are grouped per provider, andretrySend()narrows a batch to whoever is still failing. Keying off it would let a topic batch that happened to collapse to one recipient write that address into theToheader of a message theccandbccaddresses also read, and would hide 1000 subscribers of a 1001-subscriber topic while disclosing the last one.Multi-recipient batches still carry no
Toheader at all. #12365 and #10474 pull in opposite directions here and that is the side this keeps: a placeholder address inTois also an SMTP envelope recipient and would receive a copy of every batch, and the message API has no way to express theundisclosed-recipients:;group form.Test Plan
testSendEmailSmtpHeaderssends three messages through the existing Maildev fixture, one per shape the worker distinguishes:To— fails onmain;ccrecipient does not — fails against a rule keyed on batch size;composer lintand PHPStan pass on both changed files. Full Messaging E2E runs in CI.Delivery accounting and repeated CC/BCC delivery across batches are unchanged.