Skip to content

[13.x] Add SES tenant support to the SES v2 transport#60886

Open
atymic wants to merge 1 commit into
laravel:13.xfrom
atymic:feat/ses-tenant-awareness
Open

[13.x] Add SES tenant support to the SES v2 transport#60886
atymic wants to merge 1 commit into
laravel:13.xfrom
atymic:feat/ses-tenant-awareness

Conversation

@atymic

@atymic atymic commented Jul 25, 2026

Copy link
Copy Markdown

Amazon SES tenants let you isolate sending into separate containers, each with its own reputation metrics, reputation policy and suppression list. If one tenant's sending goes bad, SES pauses that tenant instead of the whole account. Useful if you send on behalf of a lot of downstream customers.

To send as a tenant you pass TenantName to SendEmail. The AWS SDK has supported this since 3.352.0, but there's no way to set it per message right now. SesV2Transport options are transport level, so a single ses mailer can't vary the tenant between messages.

This adds an X-SES-TENANT-NAME header that the transport turns into the TenantName parameter, same as the existing X-SES-LIST-MANAGEMENT-OPTIONS header does.

From a mailable:

public function headers(): Headers
{
    return new Headers(text: ['X-SES-TENANT-NAME' => 'tenant-id']);
}

Or a notification:

return (new MailMessage)->withSymfonyMessage(function ($message) {
    $message->getHeaders()->addTextHeader('X-SES-TENANT-NAME', 'tenant-id');
});

If the header isn't set, TenantName isn't passed and nothing changes.

Older SDK versions

Laravel requires aws/aws-sdk-php ^3.322.9, and TenantName only landed in 3.352.0, so I tested against 3.351.0 to check what happens on an SDK that predates it. It's safe: the SDK builds requests from its bundled API model, so an unknown key doesn't hit validation and is just left out of the request body. Nothing throws, the mail still sends, it just sends without a tenant.

I didn't bump the constraint, since that would push every SQS/DynamoDB/SES user onto a newer SDK for one optional SES feature. Happy to add a runtime exception when the installed SDK is too old if you'd prefer it fail loudly rather than silently ignore the header, just left it out for now to keep this small.

Notes

Setting the tenant in the options array in config/mail.php already works if you only have one tenant, so this is just for varying it per message.

The header stays on the outgoing message, which is the same as X-Metadata-* and X-SES-LIST-MANAGEMENT-OPTIONS today. Transports can't strip headers because SentMessage captures the raw MIME before doSend() runs.

I've used X-SES-TENANT-NAME to match the API parameter and the other X-SES-* headers, but SES uses X-SES-TENANT for its SMTP integration. Happy to rename it if you'd prefer to match that.

@atymic

atymic commented Jul 25, 2026

Copy link
Copy Markdown
Author

Test failures seem unrelated, all email tests pass for me.

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