From 19684f515dea6c76535588425a96742a97d05acc Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 08:45:46 +0530 Subject: [PATCH 1/8] feat: add Recipient value object for named email recipients 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 for to, cc, and bcc instead of array and array>. --- .../Messaging/Adapter/Email/Mailgun.php | 51 ++++++++++--------- src/Utopia/Messaging/Adapter/Email/Mock.php | 10 ++-- src/Utopia/Messaging/Adapter/Email/Resend.php | 32 ++++++------ src/Utopia/Messaging/Adapter/Email/SMTP.php | 12 ++--- .../Messaging/Adapter/Email/Sendgrid.php | 24 +++++---- src/Utopia/Messaging/Messages/Email.php | 33 +++--------- .../Messaging/Messages/Email/Recipient.php | 22 ++++++++ tests/Messaging/Adapter/Email/EmailTest.php | 13 ++--- 8 files changed, 104 insertions(+), 93 deletions(-) create mode 100644 src/Utopia/Messaging/Messages/Email/Recipient.php diff --git a/src/Utopia/Messaging/Adapter/Email/Mailgun.php b/src/Utopia/Messaging/Adapter/Email/Mailgun.php index a955b44c..6711aa7a 100644 --- a/src/Utopia/Messaging/Adapter/Email/Mailgun.php +++ b/src/Utopia/Messaging/Adapter/Email/Mailgun.php @@ -51,8 +51,15 @@ protected function process(EmailMessage $message): array $domain = $this->isEU ? $euDomain : $usDomain; + $toEmails = \array_map(fn ($to) => $to->getEmail(), $message->getTo()); + $body = [ - 'to' => \implode(',', $message->getTo()), + 'to' => \implode(',', \array_map( + fn ($to) => !empty($to->getName()) + ? "{$to->getName()}<{$to->getEmail()}>" + : $to->getEmail(), + $message->getTo() + )), 'from' => "{$message->getFromName()}<{$message->getFromEmail()}>", 'subject' => $message->getSubject(), 'text' => $message->isHtml() ? null : $message->getContent(), @@ -61,34 +68,30 @@ protected function process(EmailMessage $message): array ]; if (\count($message->getTo()) > 1) { - $body['recipient-variables'] = json_encode(array_fill_keys($message->getTo(), [])); + $body['recipient-variables'] = json_encode(array_fill_keys($toEmails, [])); } if (!\is_null($message->getCC())) { foreach ($message->getCC() as $cc) { - if (!empty($cc['email'])) { - $ccString = !empty($cc['name']) - ? "{$cc['name']}<{$cc['email']}>" - : $cc['email']; - - $body['cc'] = !empty($body['cc']) - ? "{$body['cc']},{$ccString}" - : $ccString; - } + $ccString = !empty($cc->getName()) + ? "{$cc->getName()}<{$cc->getEmail()}>" + : $cc->getEmail(); + + $body['cc'] = !empty($body['cc']) + ? "{$body['cc']},{$ccString}" + : $ccString; } } if (!\is_null($message->getBCC())) { foreach ($message->getBCC() as $bcc) { - if (!empty($bcc['email'])) { - $bccString = !empty($bcc['name']) - ? "{$bcc['name']}<{$bcc['email']}>" - : $bcc['email']; - - $body['bcc'] = !empty($body['bcc']) - ? "{$body['bcc']},{$bccString}" - : $bccString; - } + $bccString = !empty($bcc->getName()) + ? "{$bcc->getName()}<{$bcc->getEmail()}>" + : $bcc->getEmail(); + + $body['bcc'] = !empty($body['bcc']) + ? "{$body['bcc']},{$bccString}" + : $bccString; } } @@ -140,16 +143,16 @@ protected function process(EmailMessage $message): array if ($statusCode >= 200 && $statusCode < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { - $response->addResult($to); + $response->addResult($to->getEmail()); } } elseif ($statusCode >= 400 && $statusCode < 500) { foreach ($message->getTo() as $to) { if (\is_string($result['response'])) { - $response->addResult($to, $result['response']); + $response->addResult($to->getEmail(), $result['response']); } elseif (isset($result['response']['message'])) { - $response->addResult($to, $result['response']['message']); + $response->addResult($to->getEmail(), $result['response']['message']); } else { - $response->addResult($to, 'Unknown error'); + $response->addResult($to->getEmail(), 'Unknown error'); } } } diff --git a/src/Utopia/Messaging/Adapter/Email/Mock.php b/src/Utopia/Messaging/Adapter/Email/Mock.php index 54ff4e95..b6bca5dd 100644 --- a/src/Utopia/Messaging/Adapter/Email/Mock.php +++ b/src/Utopia/Messaging/Adapter/Email/Mock.php @@ -47,29 +47,29 @@ protected function process(EmailMessage $message): array $mail->isHTML($message->isHtml()); foreach ($message->getTo() as $to) { - $mail->addAddress($to); + $mail->addAddress($to->getEmail(), $to->getName()); } if (!empty($message->getCC())) { foreach ($message->getCC() as $cc) { - $mail->addCC($cc['email'], $cc['name'] ?? ''); + $mail->addCC($cc->getEmail(), $cc->getName()); } } if (!empty($message->getBCC())) { foreach ($message->getBCC() as $bcc) { - $mail->addBCC($bcc['email'], $bcc['name'] ?? ''); + $mail->addBCC($bcc->getEmail(), $bcc->getName()); } } if (!$mail->send()) { foreach ($message->getTo() as $to) { - $response->addResult($to, $mail->ErrorInfo); + $response->addResult($to->getEmail(), $mail->ErrorInfo); } } else { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { - $response->addResult($to); + $response->addResult($to->getEmail()); } } diff --git a/src/Utopia/Messaging/Adapter/Email/Resend.php b/src/Utopia/Messaging/Adapter/Email/Resend.php index 7ad0ff69..eb40e23b 100644 --- a/src/Utopia/Messaging/Adapter/Email/Resend.php +++ b/src/Utopia/Messaging/Adapter/Email/Resend.php @@ -77,11 +77,15 @@ protected function process(EmailMessage $message): array $emails = []; foreach ($message->getTo() as $to) { + $toFormatted = !empty($to->getName()) + ? "{$to->getName()} <{$to->getEmail()}>" + : $to->getEmail(); + $email = [ 'from' => $message->getFromName() ? "{$message->getFromName()} <{$message->getFromEmail()}>" : $message->getFromEmail(), - 'to' => [$to], + 'to' => [$toFormatted], 'subject' => $message->getSubject(), ]; @@ -100,11 +104,9 @@ protected function process(EmailMessage $message): array if (! \is_null($message->getCC()) && ! empty($message->getCC())) { $ccList = []; foreach ($message->getCC() as $cc) { - if (! empty($cc['email'])) { - $ccList[] = ! empty($cc['name']) - ? "{$cc['name']} <{$cc['email']}>" - : $cc['email']; - } + $ccList[] = ! empty($cc->getName()) + ? "{$cc->getName()} <{$cc->getEmail()}>" + : $cc->getEmail(); } if (! empty($ccList)) { $email['cc'] = $ccList; @@ -118,11 +120,9 @@ protected function process(EmailMessage $message): array if (! \is_null($message->getBCC()) && ! empty($message->getBCC())) { $bccList = []; foreach ($message->getBCC() as $bcc) { - if (! empty($bcc['email'])) { - $bccList[] = ! empty($bcc['name']) - ? "{$bcc['name']} <{$bcc['email']}>" - : $bcc['email']; - } + $bccList[] = ! empty($bcc->getName()) + ? "{$bcc->getName()} <{$bcc->getEmail()}>" + : $bcc->getEmail(); } if (! empty($bccList)) { $email['bcc'] = $bccList; @@ -157,9 +157,9 @@ protected function process(EmailMessage $message): array foreach ($message->getTo() as $index => $to) { if (isset($failedIndices[$index])) { - $response->addResult($to, $failedIndices[$index]); + $response->addResult($to->getEmail(), $failedIndices[$index]); } else { - $response->addResult($to); + $response->addResult($to->getEmail()); } } @@ -168,7 +168,7 @@ protected function process(EmailMessage $message): array } else { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { - $response->addResult($to); + $response->addResult($to->getEmail()); } } } elseif ($statusCode >= 400 && $statusCode < 500) { @@ -183,7 +183,7 @@ protected function process(EmailMessage $message): array } foreach ($message->getTo() as $to) { - $response->addResult($to, $errorMessage); + $response->addResult($to->getEmail(), $errorMessage); } } elseif ($statusCode >= 500) { $errorMessage = 'Server error'; @@ -195,7 +195,7 @@ protected function process(EmailMessage $message): array } foreach ($message->getTo() as $to) { - $response->addResult($to, $errorMessage); + $response->addResult($to->getEmail(), $errorMessage); } } diff --git a/src/Utopia/Messaging/Adapter/Email/SMTP.php b/src/Utopia/Messaging/Adapter/Email/SMTP.php index 28067fc8..9c7890d6 100644 --- a/src/Utopia/Messaging/Adapter/Email/SMTP.php +++ b/src/Utopia/Messaging/Adapter/Email/SMTP.php @@ -97,18 +97,18 @@ protected function process(EmailMessage $message): array $mail->AltBody = \trim($mail->AltBody); foreach ($message->getTo() as $to) { - $mail->addAddress($to); + $mail->addAddress($to->getEmail(), $to->getName()); } if (!empty($message->getCC())) { foreach ($message->getCC() as $cc) { - $mail->addCC($cc['email'], $cc['name'] ?? ''); + $mail->addCC($cc->getEmail(), $cc->getName()); } } if (!empty($message->getBCC())) { foreach ($message->getBCC() as $bcc) { - $mail->addBCC($bcc['email'], $bcc['name'] ?? ''); + $mail->addBCC($bcc->getEmail(), $bcc->getName()); } } @@ -158,7 +158,7 @@ protected function process(EmailMessage $message): array ? 'Unknown error' : $mail->ErrorInfo; - $response->addResult($to, $sent ? '' : $error); + $response->addResult($to->getEmail(), $sent ? '' : $error); } foreach ($message->getCC() as $cc) { @@ -166,7 +166,7 @@ protected function process(EmailMessage $message): array ? 'Unknown error' : $mail->ErrorInfo; - $response->addResult($cc['email'], $sent ? '' : $error); + $response->addResult($cc->getEmail(), $sent ? '' : $error); } foreach ($message->getBCC() as $bcc) { @@ -174,7 +174,7 @@ protected function process(EmailMessage $message): array ? 'Unknown error' : $mail->ErrorInfo; - $response->addResult($bcc['email'], $sent ? '' : $error); + $response->addResult($bcc->getEmail(), $sent ? '' : $error); } return $response->toArray(); diff --git a/src/Utopia/Messaging/Adapter/Email/Sendgrid.php b/src/Utopia/Messaging/Adapter/Email/Sendgrid.php index 8ea0cb01..39516700 100644 --- a/src/Utopia/Messaging/Adapter/Email/Sendgrid.php +++ b/src/Utopia/Messaging/Adapter/Email/Sendgrid.php @@ -45,7 +45,9 @@ protected function process(EmailMessage $message): array { $personalizations = \array_map( fn ($to) => [ - 'to' => [['email' => $to]], + 'to' => [!empty($to->getName()) + ? ['email' => $to->getEmail(), 'name' => $to->getName()] + : ['email' => $to->getEmail()]], 'subject' => $message->getSubject(), ], $message->getTo() @@ -54,9 +56,9 @@ protected function process(EmailMessage $message): array if (!empty($message->getCC())) { foreach ($personalizations as &$personalization) { foreach ($message->getCC() as $cc) { - $entry = ['email' => $cc['email']]; - if (!empty($cc['name'])) { - $entry['name'] = $cc['name']; + $entry = ['email' => $cc->getEmail()]; + if (!empty($cc->getName())) { + $entry['name'] = $cc->getName(); } $personalization['cc'][] = $entry; } @@ -67,9 +69,9 @@ protected function process(EmailMessage $message): array if (!empty($message->getBCC())) { foreach ($personalizations as &$personalization) { foreach ($message->getBCC() as $bcc) { - $entry = ['email' => $bcc['email']]; - if (!empty($bcc['name'])) { - $entry['name'] = $bcc['name']; + $entry = ['email' => $bcc->getEmail()]; + if (!empty($bcc->getName())) { + $entry['name'] = $bcc->getName(); } $personalization['bcc'][] = $entry; } @@ -138,16 +140,16 @@ protected function process(EmailMessage $message): array if ($statusCode === 202) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { - $response->addResult($to); + $response->addResult($to->getEmail()); } } else { foreach ($message->getTo() as $to) { if (\is_string($result['response'])) { - $response->addResult($to, $result['response']); + $response->addResult($to->getEmail(), $result['response']); } elseif (!\is_null($result['response']['errors'][0]['message'] ?? null)) { - $response->addResult($to, $result['response']['errors'][0]['message']); + $response->addResult($to->getEmail(), $result['response']['errors'][0]['message']); } else { - $response->addResult($to, 'Unknown error'); + $response->addResult($to->getEmail(), 'Unknown error'); } } } diff --git a/src/Utopia/Messaging/Messages/Email.php b/src/Utopia/Messaging/Messages/Email.php index 49c45736..5ea5c615 100644 --- a/src/Utopia/Messaging/Messages/Email.php +++ b/src/Utopia/Messaging/Messages/Email.php @@ -4,23 +4,22 @@ use Utopia\Messaging\Message; use Utopia\Messaging\Messages\Email\Attachment; +use Utopia\Messaging\Messages\Email\Recipient; class Email implements Message { /** - * @param array $to The recipients of the email. + * @param array $to The recipients of the email. * @param string $subject The subject of the email. * @param string $content The content of the email. * @param string $fromName The name of the sender. * @param string $fromEmail The email address of the sender. - * @param array>|null $cc . The CC recipients of the email. Each recipient should be an array containing a "name" and an "email" key. - * @param array>|null $bcc . The BCC recipients of the email. Each recipient should be an array containing a "name" and an "email" key. * @param string|null $replyToName The name of the reply to. * @param string|null $replyToEmail The email address of the reply to. + * @param array|null $cc The CC recipients of the email. + * @param array|null $bcc The BCC recipients of the email. * @param array|null $attachments The attachments of the email. * @param bool $html Whether the message is HTML or not. - * - * @throws \InvalidArgumentException */ public function __construct( private array $to, @@ -42,26 +41,10 @@ public function __construct( if (\is_null($this->replyToEmail)) { $this->replyToEmail = $this->fromEmail; } - - if (!\is_null($this->cc)) { - foreach ($this->cc as $recipient) { - if (!isset($recipient['email'])) { - throw new \InvalidArgumentException('Each CC recipient must have at least an email'); - } - } - } - - if (!\is_null($this->bcc)) { - foreach ($this->bcc as $recipient) { - if (!isset($recipient['email'])) { - throw new \InvalidArgumentException('Each BCC recipient must have at least an email'); - } - } - } } /** - * @return array + * @return array */ public function getTo(): array { @@ -99,7 +82,7 @@ public function getReplyToEmail(): string } /** - * @return array>|null + * @return array|null */ public function getCC(): ?array { @@ -107,7 +90,7 @@ public function getCC(): ?array } /** - * @return array>|null + * @return array|null */ public function getBCC(): ?array { @@ -115,7 +98,7 @@ public function getBCC(): ?array } /** - * @return array|null + * @return array|null */ public function getAttachments(): ?array { diff --git a/src/Utopia/Messaging/Messages/Email/Recipient.php b/src/Utopia/Messaging/Messages/Email/Recipient.php new file mode 100644 index 00000000..8494b3e1 --- /dev/null +++ b/src/Utopia/Messaging/Messages/Email/Recipient.php @@ -0,0 +1,22 @@ +email; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/tests/Messaging/Adapter/Email/EmailTest.php b/tests/Messaging/Adapter/Email/EmailTest.php index 0782fed5..a47b8e20 100644 --- a/tests/Messaging/Adapter/Email/EmailTest.php +++ b/tests/Messaging/Adapter/Email/EmailTest.php @@ -4,6 +4,7 @@ use Utopia\Messaging\Adapter\Email\Mock; use Utopia\Messaging\Messages\Email; +use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class EmailTest extends Base @@ -12,13 +13,13 @@ public function testSendEmail(): void { $sender = new Mock(); - $to = 'tester@localhost.test'; + $to = new Recipient('tester@localhost.test'); $subject = 'Test Subject'; $content = 'Test Content'; $fromName = 'Test Sender'; $fromEmail = 'sender@localhost.test'; - $cc = [['email' => 'tester2@localhost.test']]; - $bcc = [['name' => 'Tester3', 'email' => 'tester3@localhost.test']]; + $cc = [new Recipient('tester2@localhost.test')]; + $bcc = [new Recipient('tester3@localhost.test', 'Tester3')]; $message = new Email( to: [$to], @@ -35,12 +36,12 @@ public function testSendEmail(): void $lastEmail = $this->getLastEmail(); $this->assertResponse($response); - $this->assertEquals($to, $lastEmail['to'][0]['address']); + $this->assertEquals($to->getEmail(), $lastEmail['to'][0]['address']); $this->assertEquals($fromEmail, $lastEmail['from'][0]['address']); $this->assertEquals($fromName, $lastEmail['from'][0]['name']); $this->assertEquals($subject, $lastEmail['subject']); $this->assertEquals($content, \trim($lastEmail['text'])); - $this->assertEquals($cc[0]['email'], $lastEmail['cc'][0]['address']); - $this->assertEquals($bcc[0]['email'], $lastEmail['envelope']['to'][2]['address']); + $this->assertEquals($cc[0]->getEmail(), $lastEmail['cc'][0]['address']); + $this->assertEquals($bcc[0]->getEmail(), $lastEmail['envelope']['to'][2]['address']); } } From 842c1124fa2226869d19c47d2066f55402366dc8 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 08:49:08 +0530 Subject: [PATCH 2/8] fix: update all tests to use Recipient value object 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. --- src/Utopia/Messaging/Adapter/Email/Resend.php | 28 ++++++++----------- tests/Messaging/Adapter/Email/MailgunTest.php | 9 +++--- tests/Messaging/Adapter/Email/ResendTest.php | 19 +++++++------ tests/Messaging/Adapter/Email/SMTPTest.php | 18 +++++------- .../Messaging/Adapter/Email/SendgridTest.php | 9 +++--- 5 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/Email/Resend.php b/src/Utopia/Messaging/Adapter/Email/Resend.php index eb40e23b..e05a655f 100644 --- a/src/Utopia/Messaging/Adapter/Email/Resend.php +++ b/src/Utopia/Messaging/Adapter/Email/Resend.php @@ -102,15 +102,13 @@ protected function process(EmailMessage $message): array } if (! \is_null($message->getCC()) && ! empty($message->getCC())) { - $ccList = []; - foreach ($message->getCC() as $cc) { - $ccList[] = ! empty($cc->getName()) + $ccList = \array_map( + fn ($cc) => ! empty($cc->getName()) ? "{$cc->getName()} <{$cc->getEmail()}>" - : $cc->getEmail(); - } - if (! empty($ccList)) { - $email['cc'] = $ccList; - } + : $cc->getEmail(), + $message->getCC() + ); + $email['cc'] = $ccList; } if (! empty($attachments)) { @@ -118,15 +116,13 @@ protected function process(EmailMessage $message): array } if (! \is_null($message->getBCC()) && ! empty($message->getBCC())) { - $bccList = []; - foreach ($message->getBCC() as $bcc) { - $bccList[] = ! empty($bcc->getName()) + $bccList = \array_map( + fn ($bcc) => ! empty($bcc->getName()) ? "{$bcc->getName()} <{$bcc->getEmail()}>" - : $bcc->getEmail(); - } - if (! empty($bccList)) { - $email['bcc'] = $bccList; - } + : $bcc->getEmail(), + $message->getBCC() + ); + $email['bcc'] = $bccList; } $emails[] = $email; diff --git a/tests/Messaging/Adapter/Email/MailgunTest.php b/tests/Messaging/Adapter/Email/MailgunTest.php index dcba4f5b..b1184d73 100644 --- a/tests/Messaging/Adapter/Email/MailgunTest.php +++ b/tests/Messaging/Adapter/Email/MailgunTest.php @@ -5,6 +5,7 @@ use Utopia\Messaging\Adapter\Email\Mailgun; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; +use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class MailgunTest extends Base @@ -24,11 +25,11 @@ public function testSendEmail(): void $subject = 'Test Subject'; $content = 'Test Content'; $fromEmail = 'sender@'.$domain; - $cc = [['email' => \getenv('TEST_CC_EMAIL')]]; - $bcc = [['name' => \getenv('TEST_BCC_NAME'), 'email' => \getenv('TEST_BCC_EMAIL')]]; + $cc = [new Recipient(\getenv('TEST_CC_EMAIL'))]; + $bcc = [new Recipient(\getenv('TEST_BCC_EMAIL'), \getenv('TEST_BCC_NAME'))]; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: 'Test Sender', @@ -59,7 +60,7 @@ public function testSendEmailWithAttachments(): void $fromEmail = 'sender@'.$domain; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: 'Test Sender', diff --git a/tests/Messaging/Adapter/Email/ResendTest.php b/tests/Messaging/Adapter/Email/ResendTest.php index d5f87741..c2e23605 100644 --- a/tests/Messaging/Adapter/Email/ResendTest.php +++ b/tests/Messaging/Adapter/Email/ResendTest.php @@ -5,6 +5,7 @@ use Utopia\Messaging\Adapter\Email\Resend; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; +use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class ResendTest extends Base @@ -28,11 +29,11 @@ public function testSendEmail(): void $subject = 'Test Subject'; $content = 'Test Content'; $fromEmail = $this->testEmail; - $cc = [['email' => $this->testEmail]]; - $bcc = [['name' => 'Test BCC', 'email' => $this->testEmail]]; + $cc = [new Recipient($this->testEmail)]; + $bcc = [new Recipient($this->testEmail, 'Test BCC')]; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: 'Test Sender', @@ -54,7 +55,7 @@ public function testSendEmailWithHtml(): void $fromEmail = $this->testEmail; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: 'Test Sender', @@ -76,7 +77,7 @@ public function testSendEmailWithReplyTo(): void $replyToEmail = $this->testEmail; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: 'Test Sender', @@ -99,7 +100,7 @@ public function testSendMultipleEmails(): void $fromEmail = $this->testEmail; $message = new Email( - to: [$to1, $to2], + to: [new Recipient($to1), new Recipient($to2)], subject: $subject, content: $content, fromName: 'Test Sender', @@ -118,7 +119,7 @@ public function testSendMultipleEmails(): void public function testSendEmailWithFileAttachment(): void { $message = new Email( - to: [$this->testEmail], + to: [new Recipient($this->testEmail)], subject: 'Test File Attachment', content: 'Test Content with file attachment', fromName: 'Test Sender', @@ -138,7 +139,7 @@ public function testSendEmailWithFileAttachment(): void public function testSendEmailWithStringAttachment(): void { $message = new Email( - to: [$this->testEmail], + to: [new Recipient($this->testEmail)], subject: 'Test String Attachment', content: 'Test Content with string attachment', fromName: 'Test Sender', @@ -164,7 +165,7 @@ public function testSendEmailWithAttachmentExceedingMaxSize(): void $largeContent = \str_repeat('x', 25 * 1024 * 1024 + 1); $message = new Email( - to: [$this->testEmail], + to: [new Recipient($this->testEmail)], subject: 'Test Oversized Attachment', content: 'Test Content', fromName: 'Test Sender', diff --git a/tests/Messaging/Adapter/Email/SMTPTest.php b/tests/Messaging/Adapter/Email/SMTPTest.php index 08037c8e..f5ec2a9e 100644 --- a/tests/Messaging/Adapter/Email/SMTPTest.php +++ b/tests/Messaging/Adapter/Email/SMTPTest.php @@ -5,6 +5,7 @@ use Utopia\Messaging\Adapter\Email\SMTP; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; +use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class SMTPTest extends Base @@ -23,7 +24,7 @@ public function testSendEmail(): void $fromEmail = 'sender@localhost.test'; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: $fromName, @@ -55,7 +56,7 @@ public function testSendEmailWithAttachment(): void $fromEmail = 'sender@localhost.test'; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: $fromName, @@ -89,12 +90,7 @@ public function testSendEmailOnlyBCC(): void $content = 'Test Content'; $fromName = 'Test Sender'; $fromEmail = 'sender@localhost.test'; - $bcc = [ - [ - 'email' => 'tester2@localhost.test', - 'name' => 'Test Recipient 2', - ], - ]; + $bcc = [new Recipient('tester2@localhost.test', 'Test Recipient 2')]; $message = new Email( to: [], @@ -180,7 +176,7 @@ public function testSendEmailWithStringAttachment(): void $fromEmail = 'sender@localhost.test'; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: $fromName, @@ -216,7 +212,7 @@ public function testSendEmailWithKeepAlive(): void // Send first message $message1 = new Email( - to: [$to], + to: [new Recipient($to)], subject: 'KeepAlive Test 1', content: 'First message', fromName: 'Test', @@ -228,7 +224,7 @@ public function testSendEmailWithKeepAlive(): void // Send second message — should reuse the PHPMailer instance $message2 = new Email( - to: [$to], + to: [new Recipient($to)], subject: 'KeepAlive Test 2', content: 'Second message', fromName: 'Test', diff --git a/tests/Messaging/Adapter/Email/SendgridTest.php b/tests/Messaging/Adapter/Email/SendgridTest.php index 6270cfbe..3c2e2f56 100644 --- a/tests/Messaging/Adapter/Email/SendgridTest.php +++ b/tests/Messaging/Adapter/Email/SendgridTest.php @@ -5,6 +5,7 @@ use Utopia\Messaging\Adapter\Email\Sendgrid; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; +use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class SendgridTest extends Base @@ -18,11 +19,11 @@ public function testSendEmail(): void $subject = 'Test Subject'; $content = 'Test Content'; $fromEmail = \getenv('TEST_FROM_EMAIL'); - $cc = [['email' => \getenv('TEST_CC_EMAIL')]]; - $bcc = [['name' => \getenv('TEST_BCC_NAME'), 'email' => \getenv('TEST_BCC_EMAIL')]]; + $cc = [new Recipient(\getenv('TEST_CC_EMAIL'))]; + $bcc = [new Recipient(\getenv('TEST_BCC_EMAIL'), \getenv('TEST_BCC_NAME'))]; $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: 'Tester', @@ -47,7 +48,7 @@ public function testSendEmailWithAttachment(): void $fromEmail = \getenv('TEST_FROM_EMAIL'); $message = new Email( - to: [$to], + to: [new Recipient($to)], subject: $subject, content: $content, fromName: 'Tester', From d1213e017dae134ff58afc54217249900f3e3833 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 08:52:12 +0530 Subject: [PATCH 3/8] fix: address PR review comments - Add email validation in Recipient constructor to reject empty strings - Fix Mailgun address format to use RFC 5322 compliant "Name " with a space before the angle bracket, consistent with Resend adapter --- src/Utopia/Messaging/Adapter/Email/Mailgun.php | 6 +++--- src/Utopia/Messaging/Messages/Email/Recipient.php | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/Email/Mailgun.php b/src/Utopia/Messaging/Adapter/Email/Mailgun.php index 6711aa7a..ff10327a 100644 --- a/src/Utopia/Messaging/Adapter/Email/Mailgun.php +++ b/src/Utopia/Messaging/Adapter/Email/Mailgun.php @@ -56,7 +56,7 @@ protected function process(EmailMessage $message): array $body = [ 'to' => \implode(',', \array_map( fn ($to) => !empty($to->getName()) - ? "{$to->getName()}<{$to->getEmail()}>" + ? "{$to->getName()} <{$to->getEmail()}>" : $to->getEmail(), $message->getTo() )), @@ -74,7 +74,7 @@ protected function process(EmailMessage $message): array if (!\is_null($message->getCC())) { foreach ($message->getCC() as $cc) { $ccString = !empty($cc->getName()) - ? "{$cc->getName()}<{$cc->getEmail()}>" + ? "{$cc->getName()} <{$cc->getEmail()}>" : $cc->getEmail(); $body['cc'] = !empty($body['cc']) @@ -86,7 +86,7 @@ protected function process(EmailMessage $message): array if (!\is_null($message->getBCC())) { foreach ($message->getBCC() as $bcc) { $bccString = !empty($bcc->getName()) - ? "{$bcc->getName()}<{$bcc->getEmail()}>" + ? "{$bcc->getName()} <{$bcc->getEmail()}>" : $bcc->getEmail(); $body['bcc'] = !empty($body['bcc']) diff --git a/src/Utopia/Messaging/Messages/Email/Recipient.php b/src/Utopia/Messaging/Messages/Email/Recipient.php index 8494b3e1..cc42f71a 100644 --- a/src/Utopia/Messaging/Messages/Email/Recipient.php +++ b/src/Utopia/Messaging/Messages/Email/Recipient.php @@ -8,6 +8,9 @@ public function __construct( private string $email, private string $name = '', ) { + if (empty($email)) { + throw new \InvalidArgumentException('Recipient email must not be empty.'); + } } public function getEmail(): string From f1356bfea963ac52459bee55a888f38cbc6c2cb0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 08:54:34 +0530 Subject: [PATCH 4/8] feat: make Recipient value object non-breaking 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. --- src/Utopia/Messaging/Messages/Email.php | 51 ++++++++++++++++--- tests/Messaging/Adapter/Email/EmailTest.php | 13 +++-- tests/Messaging/Adapter/Email/MailgunTest.php | 9 ++-- tests/Messaging/Adapter/Email/ResendTest.php | 19 ++++--- tests/Messaging/Adapter/Email/SMTPTest.php | 18 ++++--- .../Messaging/Adapter/Email/SendgridTest.php | 9 ++-- 6 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/Utopia/Messaging/Messages/Email.php b/src/Utopia/Messaging/Messages/Email.php index 5ea5c615..7133264e 100644 --- a/src/Utopia/Messaging/Messages/Email.php +++ b/src/Utopia/Messaging/Messages/Email.php @@ -9,31 +9,50 @@ class Email implements Message { /** - * @param array $to The recipients of the email. + * @var array + */ + private array $to; + + /** + * @var array|null + */ + private ?array $cc; + + /** + * @var array|null + */ + private ?array $bcc; + + /** + * @param array> $to The recipients of the email. Each entry can be an email string, a Recipient object, or an associative array with 'email' and optional 'name' keys. * @param string $subject The subject of the email. * @param string $content The content of the email. * @param string $fromName The name of the sender. * @param string $fromEmail The email address of the sender. * @param string|null $replyToName The name of the reply to. * @param string|null $replyToEmail The email address of the reply to. - * @param array|null $cc The CC recipients of the email. - * @param array|null $bcc The BCC recipients of the email. + * @param array>|null $cc The CC recipients of the email. Same format as $to. + * @param array>|null $bcc The BCC recipients of the email. Same format as $to. * @param array|null $attachments The attachments of the email. * @param bool $html Whether the message is HTML or not. */ public function __construct( - private array $to, + array $to, private string $subject, private string $content, private string $fromName, private string $fromEmail, private ?string $replyToName = null, private ?string $replyToEmail = null, - private ?array $cc = null, - private ?array $bcc = null, + ?array $cc = null, + ?array $bcc = null, private ?array $attachments = null, private bool $html = false, ) { + $this->to = \array_map([self::class, 'toRecipient'], $to); + $this->cc = !\is_null($cc) ? \array_map([self::class, 'toRecipient'], $cc) : null; + $this->bcc = !\is_null($bcc) ? \array_map([self::class, 'toRecipient'], $bcc) : null; + if (\is_null($this->replyToName)) { $this->replyToName = $this->fromName; } @@ -43,6 +62,26 @@ public function __construct( } } + /** + * @param string|Recipient|array $value + */ + private static function toRecipient(string|Recipient|array $value): Recipient + { + if ($value instanceof Recipient) { + return $value; + } + + if (\is_string($value)) { + return new Recipient($value); + } + + if (!\is_array($value) || !isset($value['email'])) { + throw new \InvalidArgumentException('Each recipient must be a string, a Recipient object, or an array with at least an "email" key.'); + } + + return new Recipient($value['email'], $value['name'] ?? ''); + } + /** * @return array */ diff --git a/tests/Messaging/Adapter/Email/EmailTest.php b/tests/Messaging/Adapter/Email/EmailTest.php index a47b8e20..0782fed5 100644 --- a/tests/Messaging/Adapter/Email/EmailTest.php +++ b/tests/Messaging/Adapter/Email/EmailTest.php @@ -4,7 +4,6 @@ use Utopia\Messaging\Adapter\Email\Mock; use Utopia\Messaging\Messages\Email; -use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class EmailTest extends Base @@ -13,13 +12,13 @@ public function testSendEmail(): void { $sender = new Mock(); - $to = new Recipient('tester@localhost.test'); + $to = 'tester@localhost.test'; $subject = 'Test Subject'; $content = 'Test Content'; $fromName = 'Test Sender'; $fromEmail = 'sender@localhost.test'; - $cc = [new Recipient('tester2@localhost.test')]; - $bcc = [new Recipient('tester3@localhost.test', 'Tester3')]; + $cc = [['email' => 'tester2@localhost.test']]; + $bcc = [['name' => 'Tester3', 'email' => 'tester3@localhost.test']]; $message = new Email( to: [$to], @@ -36,12 +35,12 @@ public function testSendEmail(): void $lastEmail = $this->getLastEmail(); $this->assertResponse($response); - $this->assertEquals($to->getEmail(), $lastEmail['to'][0]['address']); + $this->assertEquals($to, $lastEmail['to'][0]['address']); $this->assertEquals($fromEmail, $lastEmail['from'][0]['address']); $this->assertEquals($fromName, $lastEmail['from'][0]['name']); $this->assertEquals($subject, $lastEmail['subject']); $this->assertEquals($content, \trim($lastEmail['text'])); - $this->assertEquals($cc[0]->getEmail(), $lastEmail['cc'][0]['address']); - $this->assertEquals($bcc[0]->getEmail(), $lastEmail['envelope']['to'][2]['address']); + $this->assertEquals($cc[0]['email'], $lastEmail['cc'][0]['address']); + $this->assertEquals($bcc[0]['email'], $lastEmail['envelope']['to'][2]['address']); } } diff --git a/tests/Messaging/Adapter/Email/MailgunTest.php b/tests/Messaging/Adapter/Email/MailgunTest.php index b1184d73..dcba4f5b 100644 --- a/tests/Messaging/Adapter/Email/MailgunTest.php +++ b/tests/Messaging/Adapter/Email/MailgunTest.php @@ -5,7 +5,6 @@ use Utopia\Messaging\Adapter\Email\Mailgun; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; -use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class MailgunTest extends Base @@ -25,11 +24,11 @@ public function testSendEmail(): void $subject = 'Test Subject'; $content = 'Test Content'; $fromEmail = 'sender@'.$domain; - $cc = [new Recipient(\getenv('TEST_CC_EMAIL'))]; - $bcc = [new Recipient(\getenv('TEST_BCC_EMAIL'), \getenv('TEST_BCC_NAME'))]; + $cc = [['email' => \getenv('TEST_CC_EMAIL')]]; + $bcc = [['name' => \getenv('TEST_BCC_NAME'), 'email' => \getenv('TEST_BCC_EMAIL')]]; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: 'Test Sender', @@ -60,7 +59,7 @@ public function testSendEmailWithAttachments(): void $fromEmail = 'sender@'.$domain; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: 'Test Sender', diff --git a/tests/Messaging/Adapter/Email/ResendTest.php b/tests/Messaging/Adapter/Email/ResendTest.php index c2e23605..d5f87741 100644 --- a/tests/Messaging/Adapter/Email/ResendTest.php +++ b/tests/Messaging/Adapter/Email/ResendTest.php @@ -5,7 +5,6 @@ use Utopia\Messaging\Adapter\Email\Resend; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; -use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class ResendTest extends Base @@ -29,11 +28,11 @@ public function testSendEmail(): void $subject = 'Test Subject'; $content = 'Test Content'; $fromEmail = $this->testEmail; - $cc = [new Recipient($this->testEmail)]; - $bcc = [new Recipient($this->testEmail, 'Test BCC')]; + $cc = [['email' => $this->testEmail]]; + $bcc = [['name' => 'Test BCC', 'email' => $this->testEmail]]; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: 'Test Sender', @@ -55,7 +54,7 @@ public function testSendEmailWithHtml(): void $fromEmail = $this->testEmail; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: 'Test Sender', @@ -77,7 +76,7 @@ public function testSendEmailWithReplyTo(): void $replyToEmail = $this->testEmail; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: 'Test Sender', @@ -100,7 +99,7 @@ public function testSendMultipleEmails(): void $fromEmail = $this->testEmail; $message = new Email( - to: [new Recipient($to1), new Recipient($to2)], + to: [$to1, $to2], subject: $subject, content: $content, fromName: 'Test Sender', @@ -119,7 +118,7 @@ public function testSendMultipleEmails(): void public function testSendEmailWithFileAttachment(): void { $message = new Email( - to: [new Recipient($this->testEmail)], + to: [$this->testEmail], subject: 'Test File Attachment', content: 'Test Content with file attachment', fromName: 'Test Sender', @@ -139,7 +138,7 @@ public function testSendEmailWithFileAttachment(): void public function testSendEmailWithStringAttachment(): void { $message = new Email( - to: [new Recipient($this->testEmail)], + to: [$this->testEmail], subject: 'Test String Attachment', content: 'Test Content with string attachment', fromName: 'Test Sender', @@ -165,7 +164,7 @@ public function testSendEmailWithAttachmentExceedingMaxSize(): void $largeContent = \str_repeat('x', 25 * 1024 * 1024 + 1); $message = new Email( - to: [new Recipient($this->testEmail)], + to: [$this->testEmail], subject: 'Test Oversized Attachment', content: 'Test Content', fromName: 'Test Sender', diff --git a/tests/Messaging/Adapter/Email/SMTPTest.php b/tests/Messaging/Adapter/Email/SMTPTest.php index f5ec2a9e..08037c8e 100644 --- a/tests/Messaging/Adapter/Email/SMTPTest.php +++ b/tests/Messaging/Adapter/Email/SMTPTest.php @@ -5,7 +5,6 @@ use Utopia\Messaging\Adapter\Email\SMTP; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; -use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class SMTPTest extends Base @@ -24,7 +23,7 @@ public function testSendEmail(): void $fromEmail = 'sender@localhost.test'; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: $fromName, @@ -56,7 +55,7 @@ public function testSendEmailWithAttachment(): void $fromEmail = 'sender@localhost.test'; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: $fromName, @@ -90,7 +89,12 @@ public function testSendEmailOnlyBCC(): void $content = 'Test Content'; $fromName = 'Test Sender'; $fromEmail = 'sender@localhost.test'; - $bcc = [new Recipient('tester2@localhost.test', 'Test Recipient 2')]; + $bcc = [ + [ + 'email' => 'tester2@localhost.test', + 'name' => 'Test Recipient 2', + ], + ]; $message = new Email( to: [], @@ -176,7 +180,7 @@ public function testSendEmailWithStringAttachment(): void $fromEmail = 'sender@localhost.test'; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: $fromName, @@ -212,7 +216,7 @@ public function testSendEmailWithKeepAlive(): void // Send first message $message1 = new Email( - to: [new Recipient($to)], + to: [$to], subject: 'KeepAlive Test 1', content: 'First message', fromName: 'Test', @@ -224,7 +228,7 @@ public function testSendEmailWithKeepAlive(): void // Send second message — should reuse the PHPMailer instance $message2 = new Email( - to: [new Recipient($to)], + to: [$to], subject: 'KeepAlive Test 2', content: 'Second message', fromName: 'Test', diff --git a/tests/Messaging/Adapter/Email/SendgridTest.php b/tests/Messaging/Adapter/Email/SendgridTest.php index 3c2e2f56..6270cfbe 100644 --- a/tests/Messaging/Adapter/Email/SendgridTest.php +++ b/tests/Messaging/Adapter/Email/SendgridTest.php @@ -5,7 +5,6 @@ use Utopia\Messaging\Adapter\Email\Sendgrid; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Email\Attachment; -use Utopia\Messaging\Messages\Email\Recipient; use Utopia\Tests\Adapter\Base; class SendgridTest extends Base @@ -19,11 +18,11 @@ public function testSendEmail(): void $subject = 'Test Subject'; $content = 'Test Content'; $fromEmail = \getenv('TEST_FROM_EMAIL'); - $cc = [new Recipient(\getenv('TEST_CC_EMAIL'))]; - $bcc = [new Recipient(\getenv('TEST_BCC_EMAIL'), \getenv('TEST_BCC_NAME'))]; + $cc = [['email' => \getenv('TEST_CC_EMAIL')]]; + $bcc = [['name' => \getenv('TEST_BCC_NAME'), 'email' => \getenv('TEST_BCC_EMAIL')]]; $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: 'Tester', @@ -48,7 +47,7 @@ public function testSendEmailWithAttachment(): void $fromEmail = \getenv('TEST_FROM_EMAIL'); $message = new Email( - to: [new Recipient($to)], + to: [$to], subject: $subject, content: $content, fromName: 'Tester', From b29344431d374ae637f384c3a6e526790f4f6a4b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 08:58:07 +0530 Subject: [PATCH 5/8] refactor: remove Recipient class, unify to/cc/bcc as arrays Replace the Recipient value object with a simpler approach: the to field now uses the same array> format as cc and bcc. Plain strings are auto-normalized to ['email' => $string] in the Email constructor, preserving full backward compatibility. --- .../Messaging/Adapter/Email/Mailgun.php | 48 ++++++++------- src/Utopia/Messaging/Adapter/Email/Mock.php | 10 ++-- src/Utopia/Messaging/Adapter/Email/Resend.php | 28 ++++----- src/Utopia/Messaging/Adapter/Email/SMTP.php | 12 ++-- .../Messaging/Adapter/Email/Sendgrid.php | 26 ++++---- src/Utopia/Messaging/Messages/Email.php | 59 ++++++++++++------- .../Messaging/Messages/Email/Recipient.php | 25 -------- 7 files changed, 101 insertions(+), 107 deletions(-) delete mode 100644 src/Utopia/Messaging/Messages/Email/Recipient.php diff --git a/src/Utopia/Messaging/Adapter/Email/Mailgun.php b/src/Utopia/Messaging/Adapter/Email/Mailgun.php index ff10327a..155df5d9 100644 --- a/src/Utopia/Messaging/Adapter/Email/Mailgun.php +++ b/src/Utopia/Messaging/Adapter/Email/Mailgun.php @@ -51,13 +51,13 @@ protected function process(EmailMessage $message): array $domain = $this->isEU ? $euDomain : $usDomain; - $toEmails = \array_map(fn ($to) => $to->getEmail(), $message->getTo()); + $toEmails = \array_map(fn ($to) => $to['email'], $message->getTo()); $body = [ 'to' => \implode(',', \array_map( - fn ($to) => !empty($to->getName()) - ? "{$to->getName()} <{$to->getEmail()}>" - : $to->getEmail(), + fn ($to) => !empty($to['name']) + ? "{$to['name']} <{$to['email']}>" + : $to['email'], $message->getTo() )), 'from' => "{$message->getFromName()}<{$message->getFromEmail()}>", @@ -73,25 +73,29 @@ protected function process(EmailMessage $message): array if (!\is_null($message->getCC())) { foreach ($message->getCC() as $cc) { - $ccString = !empty($cc->getName()) - ? "{$cc->getName()} <{$cc->getEmail()}>" - : $cc->getEmail(); - - $body['cc'] = !empty($body['cc']) - ? "{$body['cc']},{$ccString}" - : $ccString; + if (!empty($cc['email'])) { + $ccString = !empty($cc['name']) + ? "{$cc['name']} <{$cc['email']}>" + : $cc['email']; + + $body['cc'] = !empty($body['cc']) + ? "{$body['cc']},{$ccString}" + : $ccString; + } } } if (!\is_null($message->getBCC())) { foreach ($message->getBCC() as $bcc) { - $bccString = !empty($bcc->getName()) - ? "{$bcc->getName()} <{$bcc->getEmail()}>" - : $bcc->getEmail(); - - $body['bcc'] = !empty($body['bcc']) - ? "{$body['bcc']},{$bccString}" - : $bccString; + if (!empty($bcc['email'])) { + $bccString = !empty($bcc['name']) + ? "{$bcc['name']} <{$bcc['email']}>" + : $bcc['email']; + + $body['bcc'] = !empty($body['bcc']) + ? "{$body['bcc']},{$bccString}" + : $bccString; + } } } @@ -143,16 +147,16 @@ protected function process(EmailMessage $message): array if ($statusCode >= 200 && $statusCode < 300) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { - $response->addResult($to->getEmail()); + $response->addResult($to['email']); } } elseif ($statusCode >= 400 && $statusCode < 500) { foreach ($message->getTo() as $to) { if (\is_string($result['response'])) { - $response->addResult($to->getEmail(), $result['response']); + $response->addResult($to['email'], $result['response']); } elseif (isset($result['response']['message'])) { - $response->addResult($to->getEmail(), $result['response']['message']); + $response->addResult($to['email'], $result['response']['message']); } else { - $response->addResult($to->getEmail(), 'Unknown error'); + $response->addResult($to['email'], 'Unknown error'); } } } diff --git a/src/Utopia/Messaging/Adapter/Email/Mock.php b/src/Utopia/Messaging/Adapter/Email/Mock.php index b6bca5dd..f91b5e8e 100644 --- a/src/Utopia/Messaging/Adapter/Email/Mock.php +++ b/src/Utopia/Messaging/Adapter/Email/Mock.php @@ -47,29 +47,29 @@ protected function process(EmailMessage $message): array $mail->isHTML($message->isHtml()); foreach ($message->getTo() as $to) { - $mail->addAddress($to->getEmail(), $to->getName()); + $mail->addAddress($to['email'], $to['name'] ?? ''); } if (!empty($message->getCC())) { foreach ($message->getCC() as $cc) { - $mail->addCC($cc->getEmail(), $cc->getName()); + $mail->addCC($cc['email'], $cc['name'] ?? ''); } } if (!empty($message->getBCC())) { foreach ($message->getBCC() as $bcc) { - $mail->addBCC($bcc->getEmail(), $bcc->getName()); + $mail->addBCC($bcc['email'], $bcc['name'] ?? ''); } } if (!$mail->send()) { foreach ($message->getTo() as $to) { - $response->addResult($to->getEmail(), $mail->ErrorInfo); + $response->addResult($to['email'], $mail->ErrorInfo); } } else { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { - $response->addResult($to->getEmail()); + $response->addResult($to['email']); } } diff --git a/src/Utopia/Messaging/Adapter/Email/Resend.php b/src/Utopia/Messaging/Adapter/Email/Resend.php index e05a655f..ac15f67b 100644 --- a/src/Utopia/Messaging/Adapter/Email/Resend.php +++ b/src/Utopia/Messaging/Adapter/Email/Resend.php @@ -77,9 +77,9 @@ protected function process(EmailMessage $message): array $emails = []; foreach ($message->getTo() as $to) { - $toFormatted = !empty($to->getName()) - ? "{$to->getName()} <{$to->getEmail()}>" - : $to->getEmail(); + $toFormatted = !empty($to['name']) + ? "{$to['name']} <{$to['email']}>" + : $to['email']; $email = [ 'from' => $message->getFromName() @@ -103,9 +103,9 @@ protected function process(EmailMessage $message): array if (! \is_null($message->getCC()) && ! empty($message->getCC())) { $ccList = \array_map( - fn ($cc) => ! empty($cc->getName()) - ? "{$cc->getName()} <{$cc->getEmail()}>" - : $cc->getEmail(), + fn ($cc) => ! empty($cc['name']) + ? "{$cc['name']} <{$cc['email']}>" + : $cc['email'], $message->getCC() ); $email['cc'] = $ccList; @@ -117,9 +117,9 @@ protected function process(EmailMessage $message): array if (! \is_null($message->getBCC()) && ! empty($message->getBCC())) { $bccList = \array_map( - fn ($bcc) => ! empty($bcc->getName()) - ? "{$bcc->getName()} <{$bcc->getEmail()}>" - : $bcc->getEmail(), + fn ($bcc) => ! empty($bcc['name']) + ? "{$bcc['name']} <{$bcc['email']}>" + : $bcc['email'], $message->getBCC() ); $email['bcc'] = $bccList; @@ -153,9 +153,9 @@ protected function process(EmailMessage $message): array foreach ($message->getTo() as $index => $to) { if (isset($failedIndices[$index])) { - $response->addResult($to->getEmail(), $failedIndices[$index]); + $response->addResult($to['email'], $failedIndices[$index]); } else { - $response->addResult($to->getEmail()); + $response->addResult($to['email']); } } @@ -164,7 +164,7 @@ protected function process(EmailMessage $message): array } else { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { - $response->addResult($to->getEmail()); + $response->addResult($to['email']); } } } elseif ($statusCode >= 400 && $statusCode < 500) { @@ -179,7 +179,7 @@ protected function process(EmailMessage $message): array } foreach ($message->getTo() as $to) { - $response->addResult($to->getEmail(), $errorMessage); + $response->addResult($to['email'], $errorMessage); } } elseif ($statusCode >= 500) { $errorMessage = 'Server error'; @@ -191,7 +191,7 @@ protected function process(EmailMessage $message): array } foreach ($message->getTo() as $to) { - $response->addResult($to->getEmail(), $errorMessage); + $response->addResult($to['email'], $errorMessage); } } diff --git a/src/Utopia/Messaging/Adapter/Email/SMTP.php b/src/Utopia/Messaging/Adapter/Email/SMTP.php index 9c7890d6..c14d16be 100644 --- a/src/Utopia/Messaging/Adapter/Email/SMTP.php +++ b/src/Utopia/Messaging/Adapter/Email/SMTP.php @@ -97,18 +97,18 @@ protected function process(EmailMessage $message): array $mail->AltBody = \trim($mail->AltBody); foreach ($message->getTo() as $to) { - $mail->addAddress($to->getEmail(), $to->getName()); + $mail->addAddress($to['email'], $to['name'] ?? ''); } if (!empty($message->getCC())) { foreach ($message->getCC() as $cc) { - $mail->addCC($cc->getEmail(), $cc->getName()); + $mail->addCC($cc['email'], $cc['name'] ?? ''); } } if (!empty($message->getBCC())) { foreach ($message->getBCC() as $bcc) { - $mail->addBCC($bcc->getEmail(), $bcc->getName()); + $mail->addBCC($bcc['email'], $bcc['name'] ?? ''); } } @@ -158,7 +158,7 @@ protected function process(EmailMessage $message): array ? 'Unknown error' : $mail->ErrorInfo; - $response->addResult($to->getEmail(), $sent ? '' : $error); + $response->addResult($to['email'], $sent ? '' : $error); } foreach ($message->getCC() as $cc) { @@ -166,7 +166,7 @@ protected function process(EmailMessage $message): array ? 'Unknown error' : $mail->ErrorInfo; - $response->addResult($cc->getEmail(), $sent ? '' : $error); + $response->addResult($cc['email'], $sent ? '' : $error); } foreach ($message->getBCC() as $bcc) { @@ -174,7 +174,7 @@ protected function process(EmailMessage $message): array ? 'Unknown error' : $mail->ErrorInfo; - $response->addResult($bcc->getEmail(), $sent ? '' : $error); + $response->addResult($bcc['email'], $sent ? '' : $error); } return $response->toArray(); diff --git a/src/Utopia/Messaging/Adapter/Email/Sendgrid.php b/src/Utopia/Messaging/Adapter/Email/Sendgrid.php index 39516700..7b5620df 100644 --- a/src/Utopia/Messaging/Adapter/Email/Sendgrid.php +++ b/src/Utopia/Messaging/Adapter/Email/Sendgrid.php @@ -45,9 +45,9 @@ protected function process(EmailMessage $message): array { $personalizations = \array_map( fn ($to) => [ - 'to' => [!empty($to->getName()) - ? ['email' => $to->getEmail(), 'name' => $to->getName()] - : ['email' => $to->getEmail()]], + 'to' => [!empty($to['name']) + ? ['email' => $to['email'], 'name' => $to['name']] + : ['email' => $to['email']]], 'subject' => $message->getSubject(), ], $message->getTo() @@ -56,9 +56,9 @@ protected function process(EmailMessage $message): array if (!empty($message->getCC())) { foreach ($personalizations as &$personalization) { foreach ($message->getCC() as $cc) { - $entry = ['email' => $cc->getEmail()]; - if (!empty($cc->getName())) { - $entry['name'] = $cc->getName(); + $entry = ['email' => $cc['email']]; + if (!empty($cc['name'])) { + $entry['name'] = $cc['name']; } $personalization['cc'][] = $entry; } @@ -69,9 +69,9 @@ protected function process(EmailMessage $message): array if (!empty($message->getBCC())) { foreach ($personalizations as &$personalization) { foreach ($message->getBCC() as $bcc) { - $entry = ['email' => $bcc->getEmail()]; - if (!empty($bcc->getName())) { - $entry['name'] = $bcc->getName(); + $entry = ['email' => $bcc['email']]; + if (!empty($bcc['name'])) { + $entry['name'] = $bcc['name']; } $personalization['bcc'][] = $entry; } @@ -140,16 +140,16 @@ protected function process(EmailMessage $message): array if ($statusCode === 202) { $response->setDeliveredTo(\count($message->getTo())); foreach ($message->getTo() as $to) { - $response->addResult($to->getEmail()); + $response->addResult($to['email']); } } else { foreach ($message->getTo() as $to) { if (\is_string($result['response'])) { - $response->addResult($to->getEmail(), $result['response']); + $response->addResult($to['email'], $result['response']); } elseif (!\is_null($result['response']['errors'][0]['message'] ?? null)) { - $response->addResult($to->getEmail(), $result['response']['errors'][0]['message']); + $response->addResult($to['email'], $result['response']['errors'][0]['message']); } else { - $response->addResult($to->getEmail(), 'Unknown error'); + $response->addResult($to['email'], 'Unknown error'); } } } diff --git a/src/Utopia/Messaging/Messages/Email.php b/src/Utopia/Messaging/Messages/Email.php index 7133264e..780c361e 100644 --- a/src/Utopia/Messaging/Messages/Email.php +++ b/src/Utopia/Messaging/Messages/Email.php @@ -4,35 +4,34 @@ use Utopia\Messaging\Message; use Utopia\Messaging\Messages\Email\Attachment; -use Utopia\Messaging\Messages\Email\Recipient; class Email implements Message { /** - * @var array + * @var array> */ private array $to; /** - * @var array|null + * @var array>|null */ private ?array $cc; /** - * @var array|null + * @var array>|null */ private ?array $bcc; /** - * @param array> $to The recipients of the email. Each entry can be an email string, a Recipient object, or an associative array with 'email' and optional 'name' keys. + * @param array> $to The recipients of the email. Each entry can be an email string or an associative array with 'email' and optional 'name' keys. * @param string $subject The subject of the email. * @param string $content The content of the email. * @param string $fromName The name of the sender. * @param string $fromEmail The email address of the sender. * @param string|null $replyToName The name of the reply to. * @param string|null $replyToEmail The email address of the reply to. - * @param array>|null $cc The CC recipients of the email. Same format as $to. - * @param array>|null $bcc The BCC recipients of the email. Same format as $to. + * @param array>|null $cc The CC recipients of the email. Each recipient should be an array containing an "email" and optional "name" key. + * @param array>|null $bcc The BCC recipients of the email. Each recipient should be an array containing an "email" and optional "name" key. * @param array|null $attachments The attachments of the email. * @param bool $html Whether the message is HTML or not. */ @@ -49,9 +48,9 @@ public function __construct( private ?array $attachments = null, private bool $html = false, ) { - $this->to = \array_map([self::class, 'toRecipient'], $to); - $this->cc = !\is_null($cc) ? \array_map([self::class, 'toRecipient'], $cc) : null; - $this->bcc = !\is_null($bcc) ? \array_map([self::class, 'toRecipient'], $bcc) : null; + $this->to = \array_map([self::class, 'normalizeRecipient'], $to); + $this->cc = !\is_null($cc) ? self::validateRecipients($cc) : null; + $this->bcc = !\is_null($bcc) ? self::validateRecipients($bcc) : null; if (\is_null($this->replyToName)) { $this->replyToName = $this->fromName; @@ -63,27 +62,43 @@ public function __construct( } /** - * @param string|Recipient|array $value + * Normalize a recipient entry to an associative array with 'email' and optional 'name' keys. + * + * @param string|array $value + * @return array */ - private static function toRecipient(string|Recipient|array $value): Recipient + private static function normalizeRecipient(string|array $value): array { - if ($value instanceof Recipient) { - return $value; + if (\is_string($value)) { + return ['email' => $value]; } - if (\is_string($value)) { - return new Recipient($value); + if (!isset($value['email'])) { + throw new \InvalidArgumentException('Each recipient must have at least an "email" key.'); } - if (!\is_array($value) || !isset($value['email'])) { - throw new \InvalidArgumentException('Each recipient must be a string, a Recipient object, or an array with at least an "email" key.'); + return $value; + } + + /** + * Validate an array of recipients. + * + * @param array> $recipients + * @return array> + */ + private static function validateRecipients(array $recipients): array + { + foreach ($recipients as $recipient) { + if (!isset($recipient['email'])) { + throw new \InvalidArgumentException('Each recipient must have at least an "email" key.'); + } } - return new Recipient($value['email'], $value['name'] ?? ''); + return $recipients; } /** - * @return array + * @return array> */ public function getTo(): array { @@ -121,7 +136,7 @@ public function getReplyToEmail(): string } /** - * @return array|null + * @return array>|null */ public function getCC(): ?array { @@ -129,7 +144,7 @@ public function getCC(): ?array } /** - * @return array|null + * @return array>|null */ public function getBCC(): ?array { diff --git a/src/Utopia/Messaging/Messages/Email/Recipient.php b/src/Utopia/Messaging/Messages/Email/Recipient.php deleted file mode 100644 index cc42f71a..00000000 --- a/src/Utopia/Messaging/Messages/Email/Recipient.php +++ /dev/null @@ -1,25 +0,0 @@ -email; - } - - public function getName(): string - { - return $this->name; - } -} From 33a07cc7d99c30fabf96ca33ee2d5f09c337926f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:02:49 +0530 Subject: [PATCH 6/8] fix: address code review findings - 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 --- .../Messaging/Adapter/Email/Mailgun.php | 11 +- src/Utopia/Messaging/Adapter/Email/SMTP.php | 4 +- src/Utopia/Messaging/Messages/Email.php | 33 ++--- tests/Messaging/Adapter/Email/EmailTest.php | 133 ++++++++++++++++++ 4 files changed, 151 insertions(+), 30 deletions(-) diff --git a/src/Utopia/Messaging/Adapter/Email/Mailgun.php b/src/Utopia/Messaging/Adapter/Email/Mailgun.php index 155df5d9..63933f2e 100644 --- a/src/Utopia/Messaging/Adapter/Email/Mailgun.php +++ b/src/Utopia/Messaging/Adapter/Email/Mailgun.php @@ -51,23 +51,24 @@ protected function process(EmailMessage $message): array $domain = $this->isEU ? $euDomain : $usDomain; - $toEmails = \array_map(fn ($to) => $to['email'], $message->getTo()); + $recipients = $message->getTo(); + $toEmails = \array_map(fn ($to) => $to['email'], $recipients); $body = [ 'to' => \implode(',', \array_map( fn ($to) => !empty($to['name']) ? "{$to['name']} <{$to['email']}>" : $to['email'], - $message->getTo() + $recipients )), - 'from' => "{$message->getFromName()}<{$message->getFromEmail()}>", + 'from' => "{$message->getFromName()} <{$message->getFromEmail()}>", 'subject' => $message->getSubject(), '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()}>", ]; - if (\count($message->getTo()) > 1) { + if (\count($recipients) > 1) { $body['recipient-variables'] = json_encode(array_fill_keys($toEmails, [])); } diff --git a/src/Utopia/Messaging/Adapter/Email/SMTP.php b/src/Utopia/Messaging/Adapter/Email/SMTP.php index c14d16be..7179f505 100644 --- a/src/Utopia/Messaging/Adapter/Email/SMTP.php +++ b/src/Utopia/Messaging/Adapter/Email/SMTP.php @@ -161,7 +161,7 @@ protected function process(EmailMessage $message): array $response->addResult($to['email'], $sent ? '' : $error); } - foreach ($message->getCC() as $cc) { + foreach ($message->getCC() ?? [] as $cc) { $error = empty($mail->ErrorInfo) ? 'Unknown error' : $mail->ErrorInfo; @@ -169,7 +169,7 @@ protected function process(EmailMessage $message): array $response->addResult($cc['email'], $sent ? '' : $error); } - foreach ($message->getBCC() as $bcc) { + foreach ($message->getBCC() ?? [] as $bcc) { $error = empty($mail->ErrorInfo) ? 'Unknown error' : $mail->ErrorInfo; diff --git a/src/Utopia/Messaging/Messages/Email.php b/src/Utopia/Messaging/Messages/Email.php index 780c361e..550431cb 100644 --- a/src/Utopia/Messaging/Messages/Email.php +++ b/src/Utopia/Messaging/Messages/Email.php @@ -30,8 +30,8 @@ class Email implements Message * @param string $fromEmail The email address of the sender. * @param string|null $replyToName The name of the reply to. * @param string|null $replyToEmail The email address of the reply to. - * @param array>|null $cc The CC recipients of the email. Each recipient should be an array containing an "email" and optional "name" key. - * @param array>|null $bcc The BCC recipients of the email. Each recipient should be an array containing an "email" and optional "name" key. + * @param array>|null $cc The CC recipients of the email. Same format as $to. + * @param array>|null $bcc The BCC recipients of the email. Same format as $to. * @param array|null $attachments The attachments of the email. * @param bool $html Whether the message is HTML or not. */ @@ -49,8 +49,8 @@ public function __construct( private bool $html = false, ) { $this->to = \array_map([self::class, 'normalizeRecipient'], $to); - $this->cc = !\is_null($cc) ? self::validateRecipients($cc) : null; - $this->bcc = !\is_null($bcc) ? self::validateRecipients($bcc) : null; + $this->cc = !\is_null($cc) ? \array_map([self::class, 'normalizeRecipient'], $cc) : null; + $this->bcc = !\is_null($bcc) ? \array_map([self::class, 'normalizeRecipient'], $bcc) : null; if (\is_null($this->replyToName)) { $this->replyToName = $this->fromName; @@ -70,33 +70,20 @@ public function __construct( private static function normalizeRecipient(string|array $value): array { if (\is_string($value)) { + if ($value === '') { + throw new \InvalidArgumentException('Recipient email must not be empty.'); + } + return ['email' => $value]; } - if (!isset($value['email'])) { - throw new \InvalidArgumentException('Each recipient must have at least an "email" key.'); + if (!isset($value['email']) || $value['email'] === '') { + throw new \InvalidArgumentException('Each recipient must have a non-empty "email" key.'); } return $value; } - /** - * Validate an array of recipients. - * - * @param array> $recipients - * @return array> - */ - private static function validateRecipients(array $recipients): array - { - foreach ($recipients as $recipient) { - if (!isset($recipient['email'])) { - throw new \InvalidArgumentException('Each recipient must have at least an "email" key.'); - } - } - - return $recipients; - } - /** * @return array> */ diff --git a/tests/Messaging/Adapter/Email/EmailTest.php b/tests/Messaging/Adapter/Email/EmailTest.php index 0782fed5..3f8a4dcf 100644 --- a/tests/Messaging/Adapter/Email/EmailTest.php +++ b/tests/Messaging/Adapter/Email/EmailTest.php @@ -43,4 +43,137 @@ public function testSendEmail(): void $this->assertEquals($cc[0]['email'], $lastEmail['cc'][0]['address']); $this->assertEquals($bcc[0]['email'], $lastEmail['envelope']['to'][2]['address']); } + + public function testSendEmailWithNamedToRecipient(): void + { + $sender = new Mock(); + + $message = new Email( + to: [['email' => 'tester@localhost.test', 'name' => 'Test User']], + subject: 'Named To Test', + content: 'Test Content', + fromName: 'Test Sender', + fromEmail: 'sender@localhost.test', + ); + + $response = $sender->send($message); + + $lastEmail = $this->getLastEmail(); + + $this->assertResponse($response); + $this->assertEquals('tester@localhost.test', $lastEmail['to'][0]['address']); + $this->assertEquals('Test User', $lastEmail['to'][0]['name']); + } + + public function testSendEmailWithMixedToFormats(): void + { + $sender = new Mock(); + + $message = new Email( + to: [ + 'plain@localhost.test', + ['email' => 'named@localhost.test', 'name' => 'Named User'], + ], + subject: 'Mixed To Test', + content: 'Test Content', + fromName: 'Test Sender', + fromEmail: 'sender@localhost.test', + ); + + $response = $sender->send($message); + + $this->assertResponse($response); + + // Verify both recipients are normalized to array format + $to = $message->getTo(); + $this->assertEquals('plain@localhost.test', $to[0]['email']); + $this->assertArrayNotHasKey('name', $to[0]); + $this->assertEquals('named@localhost.test', $to[1]['email']); + $this->assertEquals('Named User', $to[1]['name']); + } + + public function testCcAcceptsPlainStrings(): void + { + $message = new Email( + to: ['tester@localhost.test'], + subject: 'CC String Test', + content: 'Test Content', + fromName: 'Test Sender', + fromEmail: 'sender@localhost.test', + cc: ['cc@localhost.test'], + ); + + $cc = $message->getCC(); + $this->assertNotNull($cc); + $this->assertEquals('cc@localhost.test', $cc[0]['email']); + } + + public function testBccAcceptsPlainStrings(): void + { + $message = new Email( + to: ['tester@localhost.test'], + subject: 'BCC String Test', + content: 'Test Content', + fromName: 'Test Sender', + fromEmail: 'sender@localhost.test', + bcc: ['bcc@localhost.test'], + ); + + $bcc = $message->getBCC(); + $this->assertNotNull($bcc); + $this->assertEquals('bcc@localhost.test', $bcc[0]['email']); + } + + public function testRejectsEmptyEmailString(): void + { + $this->expectException(\InvalidArgumentException::class); + + new Email( + to: [''], + subject: 'Test', + content: 'Test', + fromName: 'Test', + fromEmail: 'sender@localhost.test', + ); + } + + public function testRejectsEmptyEmailInArray(): void + { + $this->expectException(\InvalidArgumentException::class); + + new Email( + to: [['email' => '', 'name' => 'Ghost']], + subject: 'Test', + content: 'Test', + fromName: 'Test', + fromEmail: 'sender@localhost.test', + ); + } + + public function testRejectsMissingEmailKey(): void + { + $this->expectException(\InvalidArgumentException::class); + + new Email( + to: [['name' => 'No Email']], + subject: 'Test', + content: 'Test', + fromName: 'Test', + fromEmail: 'sender@localhost.test', + ); + } + + public function testRejectsEmptyEmailInCc(): void + { + $this->expectException(\InvalidArgumentException::class); + + new Email( + to: ['valid@localhost.test'], + subject: 'Test', + content: 'Test', + fromName: 'Test', + fromEmail: 'sender@localhost.test', + cc: [''], + ); + } } From 92aaa2772cf4a67b64590a10634f24126cb7f608 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:05:05 +0530 Subject: [PATCH 7/8] fix: assert correct deliveredTo count in mixed-format test The test sends 2 recipients so it can't use assertResponse which hardcodes deliveredTo === 1. Assert the count and status directly. --- tests/Messaging/Adapter/Email/EmailTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Messaging/Adapter/Email/EmailTest.php b/tests/Messaging/Adapter/Email/EmailTest.php index 3f8a4dcf..7997aac9 100644 --- a/tests/Messaging/Adapter/Email/EmailTest.php +++ b/tests/Messaging/Adapter/Email/EmailTest.php @@ -82,7 +82,9 @@ public function testSendEmailWithMixedToFormats(): void $response = $sender->send($message); - $this->assertResponse($response); + $this->assertEquals(2, $response['deliveredTo']); + $this->assertEquals('success', $response['results'][0]['status']); + $this->assertEquals('success', $response['results'][1]['status']); // Verify both recipients are normalized to array format $to = $message->getTo(); From cf050ec33a48fa2e01c62fd39f0b0bf9df7334aa Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 2 Apr 2026 09:39:09 +0530 Subject: [PATCH 8/8] chore: bump PHP requirement to 8.1+, use first-class callables 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. --- composer.json | 2 +- composer.lock | 49 +++++++++++++------------ src/Utopia/Messaging/Messages/Email.php | 6 +-- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index 1bdaedca..db7c1ff3 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } }, "require": { - "php": ">=8.0.0", + "php": ">=8.1.0", "ext-curl": "*", "ext-openssl": "*", "phpmailer/phpmailer": "6.9.1", diff --git a/composer.lock b/composer.lock index e341ecad..b7856622 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dacd89729811e12e6007426189589c80", + "content-hash": "fb98182a4b49a3d30c785a6e888722b3", "packages": [ { "name": "giggsey/libphonenumber-for-php-lite", @@ -254,16 +254,16 @@ "packages-dev": [ { "name": "laravel/pint", - "version": "v1.27.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90", - "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -274,13 +274,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.92.4", - "illuminate/view": "^12.44.0", - "larastan/larastan": "^3.8.1", - "laravel-zero/framework": "^12.0.4", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.3", - "pestphp/pest": "^3.8.4" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" @@ -317,7 +318,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-01-05T16:49:17+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { "name": "myclabs/deep-copy", @@ -557,11 +558,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "1.12.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", + "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", "shasum": "" }, "require": { @@ -606,7 +607,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2026-02-28T20:30:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -957,16 +958,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.51", + "version": "11.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ad14159f92910b0f0e3928c13e9b2077529de091" + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad14159f92910b0f0e3928c13e9b2077529de091", - "reference": "ad14159f92910b0f0e3928c13e9b2077529de091", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", "shasum": "" }, "require": { @@ -1039,7 +1040,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.51" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" }, "funding": [ { @@ -1063,7 +1064,7 @@ "type": "tidelift" } ], - "time": "2026-02-05T07:59:30+00:00" + "time": "2026-02-18T12:37:06+00:00" }, { "name": "sebastian/cli-parser", @@ -2160,7 +2161,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.0.0", + "php": ">=8.1.0", "ext-curl": "*", "ext-openssl": "*" }, @@ -2168,5 +2169,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Utopia/Messaging/Messages/Email.php b/src/Utopia/Messaging/Messages/Email.php index 550431cb..877394a4 100644 --- a/src/Utopia/Messaging/Messages/Email.php +++ b/src/Utopia/Messaging/Messages/Email.php @@ -48,9 +48,9 @@ public function __construct( private ?array $attachments = null, private bool $html = false, ) { - $this->to = \array_map([self::class, 'normalizeRecipient'], $to); - $this->cc = !\is_null($cc) ? \array_map([self::class, 'normalizeRecipient'], $cc) : null; - $this->bcc = !\is_null($bcc) ? \array_map([self::class, 'normalizeRecipient'], $bcc) : null; + $this->to = \array_map(self::normalizeRecipient(...), $to); + $this->cc = !\is_null($cc) ? \array_map(self::normalizeRecipient(...), $cc) : null; + $this->bcc = !\is_null($bcc) ? \array_map(self::normalizeRecipient(...), $bcc) : null; if (\is_null($this->replyToName)) { $this->replyToName = $this->fromName;