Skip to content

feat: add name support to email 'to' field, matching cc/bcc format#114

Merged
ChiragAgg5k merged 8 commits intomainfrom
feat-recipient-value-object
Apr 2, 2026
Merged

feat: add name support to email 'to' field, matching cc/bcc format#114
ChiragAgg5k merged 8 commits intomainfrom
feat-recipient-value-object

Conversation

@ChiragAgg5k
Copy link
Copy Markdown
Member

@ChiragAgg5k ChiragAgg5k commented Apr 2, 2026

Summary

  • Extends the to field on the Email message class to accept the same array<array<string,string>> format as cc and bcc, enabling recipient names (e.g. ['email' => '...', 'name' => '...'])
  • Fully backward-compatible: plain strings like ['alice@example.com'] are auto-normalized to [['email' => 'alice@example.com']] in the constructor
  • Updates all 5 email adapters (Mailgun, SendGrid, SMTP, Resend, Mock) to read to entries via array access, formatting named recipients as "Name <email>" where supported
  • Fixes Mailgun address format to use RFC 5322 compliant "Name <email>" (with space before angle bracket)
  • All existing tests pass unchanged, proving backward compatibility

Usage

// Existing usage — still works, no changes needed
new Email(to: ['alice@example.com'], ...);

// New: add names to 'to' recipients, same format as cc/bcc
new Email(
    to: [['email' => 'alice@example.com', 'name' => 'Alice']],
    cc: [['email' => 'bob@example.com', 'name' => 'Bob']],
    ...
);

// Mix strings and arrays
new Email(
    to: ['alice@example.com', ['email' => 'bob@example.com', 'name' => 'Bob']],
    ...
);

Test plan

  • Run existing test suite — all tests use original string syntax to verify backward compatibility
  • Verify each adapter formats named to recipients correctly (e.g. "Alice <alice@example.com>")
  • Verify validation rejects entries missing the email key

Introduce a Recipient class to replace raw string arrays for to, cc,
and bcc fields. This provides a consistent, type-safe API for
associating names with email addresses across all fields and adapters.

BREAKING CHANGE: Email constructor now accepts array<Recipient> for
to, cc, and bcc instead of array<string> and array<array<string,string>>.
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 2, 2026

Greptile Summary

This PR extends the Email message class's to field to accept the same array<array<string,string>> format already used by cc and bcc, enabling named recipients. Plain string entries are auto-normalized in the constructor for full backward compatibility. All five email adapters are updated consistently, and seven new tests cover the new format, mixed inputs, and validation error paths.

  • Email.php — introduces normalizeRecipient(), a clean static helper that unifies input handling for to, cc, and bcc and validates non-empty emails at construction time.
  • Adapters — each adapter correctly reads $to['email'] and $to['name'] ?? ''; SendGrid uses its native structured object format while the others build \"Name <email>\" strings.
  • Mailgun — the h:Reply-To body entry (line 68) is still missing the => assignment operator, making it a numeric-indexed array value rather than a keyed header field. Mailgun will never receive this header.
  • PHP 8.1 minimum version bump — the first-class callable syntax (self::normalizeRecipient(...)) requires PHP 8.1; composer.json is updated accordingly, but consumers still on PHP 8.0 will encounter a dependency conflict.

Confidence Score: 4/5

Safe to merge after fixing the Mailgun Reply-To header key bug on line 68.

One P1 finding remains: the Mailgun h:Reply-To entry is a numeric-indexed array value (missing =>), so the Reply-To header is silently dropped for all Mailgun emails. All other changes are well-structured and tested. The P2 display-name quoting note is a hardening suggestion, not a blocker.

src/Utopia/Messaging/Adapter/Email/Mailgun.php — line 68 has a pre-existing bug (missing => on the h:Reply-To entry) that this PR touches but does not fix.

Important Files Changed

Filename Overview
src/Utopia/Messaging/Messages/Email.php Adds normalizeRecipient to unify to, cc, and bcc into array<array<string,string>>; validates non-empty email; fully backward-compatible.
src/Utopia/Messaging/Adapter/Email/Mailgun.php Updated to read to entries as arrays and fixed CC/BCC space formatting; however, the pre-existing h:Reply-To entry is still a numeric-indexed array value (missing =>), meaning the Reply-To header is never sent.
src/Utopia/Messaging/Adapter/Email/Resend.php Correctly updated to format to recipients with optional name; CC/BCC loops simplified to array_map — safe because normalizeRecipient now guarantees non-empty emails.
src/Utopia/Messaging/Adapter/Email/SMTP.php Passes $to['email'] and $to['name'] ?? '' to PHPMailer; added ?? [] null-coalescing for CC/BCC result loops — clean and correct.
src/Utopia/Messaging/Adapter/Email/Sendgrid.php Uses SendGrid's native {email, name} object format for named to recipients, consistent with existing CC/BCC handling.
src/Utopia/Messaging/Adapter/Email/Mock.php Updated to pass $to['email'] and $to['name'] ?? '' to PHPMailer's addAddress — straightforward and correct.
tests/Messaging/Adapter/Email/EmailTest.php Adds 7 new tests covering named recipients, mixed formats, plain-string normalization for CC/BCC, and validation error paths — good coverage of the new feature.
composer.json Bumps minimum PHP requirement from 8.0 to 8.1 to support the first-class callable syntax (self::normalizeRecipient(...)) — intentional but a potentially breaking change for consumers still on PHP 8.0.

Reviews (5): Last reviewed commit: "chore: bump PHP requirement to 8.1+, use..." | Re-trigger Greptile

Update MailgunTest, SendgridTest, ResendTest, and SMTPTest to
construct Recipient objects instead of raw strings/arrays. Also
fix PHPStan warning in Resend adapter for always-truthy empty() check.
- Add email validation in Recipient constructor to reject empty strings
- Fix Mailgun address format to use RFC 5322 compliant "Name <email>"
  with a space before the angle bracket, consistent with Resend adapter
The Email constructor now accepts strings, associative arrays, or
Recipient objects for to, cc, and bcc fields. Inputs are normalized
to Recipient objects internally so adapters remain unchanged. This
preserves full backward compatibility with the existing API while
adding support for named recipients via the new Recipient class.
@ChiragAgg5k ChiragAgg5k changed the title feat: add Recipient value object for named email recipients feat: add Recipient value object with backward-compatible named email support Apr 2, 2026
Replace the Recipient value object with a simpler approach: the to
field now uses the same array<array<string,string>> format as cc and
bcc. Plain strings are auto-normalized to ['email' => $string] in
the Email constructor, preserving full backward compatibility.
@ChiragAgg5k ChiragAgg5k changed the title feat: add Recipient value object with backward-compatible named email support feat: add name support to email 'to' field, matching cc/bcc format Apr 2, 2026
- Unify normalization: cc/bcc now go through normalizeRecipient just
  like to, so all three fields accept both plain strings and arrays
- Reject empty email strings in normalizeRecipient (was only checking
  isset, which passes for empty strings)
- Remove separate validateRecipients method (redundant with unified
  normalizeRecipient)
- Fix Mailgun from/reply-to format to use RFC 5322 space before <
- Fix SMTP null guard on getCC()/getBCC() in result-reporting loop
- Cache getTo() result in Mailgun to avoid redundant calls
- Add tests for named to recipients, mixed string/array inputs,
  cc/bcc string normalization, and validation rejection of empty
  emails and missing email keys
The test sends 2 recipients so it can't use assertResponse which
hardcodes deliveredTo === 1. Assert the count and status directly.
Raise minimum PHP version from 8.0 to 8.1 in composer.json and
use first-class callable syntax (self::method(...)) for array_map
calls in the Email constructor.
@ChiragAgg5k ChiragAgg5k merged commit a6ac04f into main Apr 2, 2026
3 of 4 checks passed
@ChiragAgg5k ChiragAgg5k deleted the feat-recipient-value-object branch April 2, 2026 04:09
'text' => $message->isHtml() ? null : $message->getContent(),
'html' => $message->isHtml() ? $message->getContent() : null,
'h:Reply-To: '."{$message->getReplyToName()}<{$message->getReplyToEmail()}>",
'h:Reply-To: '."{$message->getReplyToName()} <{$message->getReplyToEmail()}>",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 h:Reply-To entry is a numeric-indexed value, not a keyed header

The array entry on this line is missing the => assignment operator, so the entire string 'h:Reply-To: Name <email>' is stored as a numeric-indexed element (e.g. $body[0]). When form-encoded and sent to Mailgun this becomes 0=h%3AReply-To%3A+... rather than the h:Reply-To custom-header field Mailgun expects. The Reply-To header is therefore silently never forwarded.

This is a pre-existing bug, but this PR already touches the line (adding the RFC 5322 space), making it the natural place to fix it:

Suggested change
'h:Reply-To: '."{$message->getReplyToName()} <{$message->getReplyToEmail()}>",
'h:Reply-To' => "{$message->getReplyToName()} <{$message->getReplyToEmail()}>",

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.

2 participants