You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds an optional headers array to Messages\Email. It is validated when the message is constructed, and bad input throws the new header_malformed type. The checks reject:
CR, LF and NUL in values
names that aren't token-safe
names the message owns (From, To, Subject, Content-Type and the rest of the smtp Message reserved list)
two names that differ only in case
How each adapter sends the headers:
SMTP, SES raw and Mock: merged in Mime::message(). The adapter's own headers, such as X-Mailer, still win.
SES bulk: sent as DefaultContent.Template.Headers.
Resend: a per-email headers field.
Mailgun: h: form fields.
Sendgrid: a top-level headers object.
Headers go to every recipient of a message. A per-recipient value, such as an unsubscribe token, needs one Email per recipient.
This is needed for RFC 8058 one-click unsubscribe (List-Unsubscribe / List-Unsubscribe-Post) in Appwrite Cloud mail. The change is additive, so it ships as a minor release.
Tests:
Unit coverage for the message validation, Mime, SES bulk and raw, Resend, Mailgun and Sendgrid.
An e2e SMTP case against mailcatcher. It has not been run locally, so the CI e2e tier is its first run.
Release required: after merge this needs a messaging/<next minor> tag. The consumer PRs pin this branch through Split Dev (utopia-php/messaging:dev-feat/messaging-email-headers) until that release exists.
Related draft PRs:
appwrite/appwrite: server-ce mail queue headers (link to follow)
appwrite-labs/cloud: one-click unsubscribe (link to follow)
appwrite/vibes: email preferences page (link to follow)
### Issue 1
packages/messaging/tests/Messaging/Adapter/Email/SendgridHeadersTest.php:undefined-27
This assertion copies the exact internal `$body['headers']` request path instead of testing observable email behavior. The new Mailgun and SES tests repeat the same pattern by asserting exact `h:` fields and SES request nesting. These tests will fail after harmless request-builder refactors while failing to detect whether recipients actually receive the headers. This violates the repository directive to test observable behavior rather than mirror source structures, so the requirement must be satisfied before merging.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
This PR adds validated custom email headers to the shared email model and propagates them through SMTP/MIME, SES, Resend, Mailgun, and SendGrid.
Rejects malformed, reserved, empty, and case-duplicate headers.
Preserves adapter-owned headers during case-insensitive collisions.
Adds provider routing coverage and an end-to-end SMTP delivery check.
The previously reported reliance on provider request internals remains unresolved.
Malformed UTF-8 values still pass this validation. On the MIME path, the SMTP header encoder's UTF-8 split then fails and renders an empty value, while JSON-based adapters cannot encode the same input, so one accepted Email behaves differently by adapter. Reject invalid UTF-8 here and add a malformed-byte test.
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
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.
Adds an optional
headersarray toMessages\Email. It is validated when the message is constructed, and bad input throws the newheader_malformedtype. The checks reject:Messagereserved list)How each adapter sends the headers:
Mime::message(). The adapter's own headers, such as X-Mailer, still win.DefaultContent.Template.Headers.headersfield.h:form fields.headersobject.Headers go to every recipient of a message. A per-recipient value, such as an unsubscribe token, needs one
Emailper recipient.This is needed for RFC 8058 one-click unsubscribe (
List-Unsubscribe/List-Unsubscribe-Post) in Appwrite Cloud mail. The change is additive, so it ships as a minor release.Tests:
Release required: after merge this needs a
messaging/<next minor>tag. The consumer PRs pin this branch through Split Dev (utopia-php/messaging:dev-feat/messaging-email-headers) until that release exists.Related draft PRs: