-
Couldn't load subscription status.
- Fork 66
feat: add Resend email adapter #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add support for Resend email service with a new adapter implementation and comprehensive tests.
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a new Resend email adapter at src/Utopia/Messaging/Adapter/Email/Resend.php that sends batch emails to https://api.resend.com/emails/batch (supports per-recipient to/cc/bcc, from/name, subject, HTML/text, optional reply-to; disallows attachments; max 100 messages per request) and maps API responses to per-recipient results and errors. Introduces tests at tests/Messaging/Adapter/Email/ResendTest.php covering plain text, HTML, reply-to, multiple recipients, and attachment rejection. Adds RESEND_API_KEY and RESEND_TEST_EMAIL environment variables to .env.dev, docker-compose.yml, and the GitHub Actions test workflow. Updates README to document Resend usage. Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds a new Resend email adapter to the messaging library, enabling email delivery through Resend's API. The implementation follows the existing adapter pattern used by SendGrid and Mailgun.
Key Changes:
- New
Resendadapter class with batch email sending support (up to 100 emails per request) - Comprehensive test suite covering basic functionality, HTML emails, reply-to, batch sending, and attachment validation
- Environment configuration and documentation updates
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Utopia/Messaging/Adapter/Email/Resend.php | Implements the Resend email adapter with batch sending, HTML support, CC/BCC, and reply-to functionality |
| tests/Messaging/Adapter/Email/ResendTest.php | Provides test coverage for all Resend adapter features including error cases |
| docker-compose.yml | Adds RESEND_API_KEY environment variable for Docker configuration |
| .github/workflows/test.yml | Adds RESEND_API_KEY secret for CI/CD testing |
| .env.dev | Adds RESEND_API_KEY placeholder for local development |
| README.md | Documents Resend usage example and adds to supported adapters list |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (14)
README.md (1)
40-41: Document Resend limitations (attachments unsupported; 100 recipients per request).Add a short note under the code example so users don’t hit unexpected errors.
Proposed addition after the code block:
```php $messaging = new Resend('YOUR_API_KEY'); $messaging->send($message);+> Notes for Resend:
+> - Attachments are not supported by this adapter.
+> - Max 100 recipients per request; larger batches will throw.</blockquote></details> <details> <summary>docker-compose.yml (1)</summary><blockquote> `13-13`: **Compose env addition looks good.** Optional: keep env list alphabetized (move RESEND_API_KEY above SENDGRID_API_KEY) for consistency with .env.dev. </blockquote></details> <details> <summary>tests/Messaging/Adapter/Email/ResendTest.php (7)</summary><blockquote> `14-16`: **Skip test when RESEND_API_KEY is not set to ease local runs.** Prevents spurious failures outside CI. Apply: ```diff $key = \getenv('RESEND_API_KEY'); -$sender = new Resend($key); +if (!$key) { + $this->markTestSkipped('RESEND_API_KEY not set'); +} +$sender = new Resend($key);
41-43: Repeat skip guard here.Same change as above:
$key = \getenv('RESEND_API_KEY'); -$sender = new Resend($key); +if (!$key) { + $this->markTestSkipped('RESEND_API_KEY not set'); +} +$sender = new Resend($key);
65-67: Repeat skip guard here.$key = \getenv('RESEND_API_KEY'); -$sender = new Resend($key); +if (!$key) { + $this->markTestSkipped('RESEND_API_KEY not set'); +} +$sender = new Resend($key);
91-93: Repeat skip guard here.$key = \getenv('RESEND_API_KEY'); -$sender = new Resend($key); +if (!$key) { + $this->markTestSkipped('RESEND_API_KEY not set'); +} +$sender = new Resend($key);
110-115: Reduce assertion duplication in batch test.Loop through all results; assert both success entries uniformly.
Apply:
- $this->assertEquals(2, $response['deliveredTo'], \var_export($response, true)); - $this->assertEquals('', $response['results'][0]['error'], \var_export($response, true)); - $this->assertEquals('success', $response['results'][0]['status'], \var_export($response, true)); - $this->assertEquals('', $response['results'][1]['error'], \var_export($response, true)); - $this->assertEquals('success', $response['results'][1]['status'], \var_export($response, true)); + $this->assertEquals(2, $response['deliveredTo'], \var_export($response, true)); + foreach ($response['results'] as $res) { + $this->assertSame('', $res['error'], \var_export($response, true)); + $this->assertSame('success', $res['status'], \var_export($response, true)); + }
122-124: Repeat skip guard here.$key = \getenv('RESEND_API_KEY'); -$sender = new Resend($key); +if (!$key) { + $this->markTestSkipped('RESEND_API_KEY not set'); +} +$sender = new Resend($key);
117-141: Add a test for the 100-recipient cap.Ensure the adapter throws when >100 recipients to lock in behavior.
Do you want me to draft this test (e.g., generate 101 emails and assert exception message)?
src/Utopia/Messaging/Adapter/Email/Resend.php (5)
67-79: Minor: simplify null/empty checks.
!empty($message->getCC())is sufficient (same for BCC below).Apply:
- if (! \is_null($message->getCC()) && ! empty($message->getCC())) { + if (! empty($message->getCC())) {
81-93: Minor: simplify null/empty checks (BCC).Apply:
- if (! \is_null($message->getBCC()) && ! empty($message->getBCC())) { + if (! empty($message->getBCC())) {
98-101: AddAccept: application/jsonheader.Improves content negotiation and error responses.
Apply:
$headers = [ 'Authorization: Bearer '.$this->apiKey, 'Content-Type: application/json', + 'Accept: application/json', ];
137-163: Handle network/unknown status codes (e.g., 0, 3xx).Currently unhandled paths yield empty results; propagate a useful error to all recipients.
Apply:
} elseif ($statusCode >= 500) { $errorMessage = 'Server error'; @@ foreach ($message->getTo() as $to) { $response->addResult($to, $errorMessage); } - } + } else { + $errorMessage = 'Request failed'; + if (!empty($result['error']) && \is_string($result['error'])) { + $errorMessage = $result['error']; + } elseif (\is_string($result['response'])) { + $errorMessage = $result['response']; + } + foreach ($message->getTo() as $to) { + $response->addResult($to, $errorMessage); + } + }
45-96: Optional: extract address formatting to a helper.Reduces duplication for From/CC/BCC/Reply-To rendering.
Example:
private function fmt(?string $name, string $email): string { return !empty($name) ? "{$name} <{$email}>" : $email; }Then use:
- 'from' => $this->fmt($message->getFromName(), $message->getFromEmail())
- $ccList[] = $this->fmt($cc['name'] ?? '', $cc['email'])
- etc.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
.env.dev(1 hunks).github/workflows/test.yml(1 hunks)README.md(3 hunks)docker-compose.yml(1 hunks)src/Utopia/Messaging/Adapter/Email/Resend.php(1 hunks)tests/Messaging/Adapter/Email/ResendTest.php(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/Messaging/Adapter/Email/ResendTest.php (4)
src/Utopia/Messaging/Adapter.php (2)
Adapter(8-285)send(45-58)src/Utopia/Messaging/Adapter/Email/Resend.php (1)
Resend(9-167)src/Utopia/Messaging/Messages/Email/Attachment.php (1)
Attachment(5-33)tests/Messaging/Adapter/Base.php (1)
assertResponse(41-46)
src/Utopia/Messaging/Adapter/Email/Resend.php (3)
src/Utopia/Messaging/Adapter.php (2)
Adapter(8-285)request(77-132)src/Utopia/Messaging/Response.php (4)
Response(5-76)addResult(56-63)setDeliveredTo(23-26)toArray(68-75)src/Utopia/Messaging/Messages/Email.php (8)
getFromName(81-84)getFromEmail(86-89)getSubject(71-74)isHtml(125-128)getReplyToEmail(96-99)getReplyToName(91-94)getCC(104-107)getBCC(112-115)
🪛 dotenv-linter (4.0.0)
.env.dev
[warning] 4-4: [UnorderedKey] The RESEND_API_KEY key should go before the SENDGRID_API_KEY key
(UnorderedKey)
🔇 Additional comments (4)
README.md (1)
26-26: Import looks good..github/workflows/test.yml (1)
20-20: CI wiring looks correct; ensure secret exists.Verify repository/org secret RESEND_API_KEY is set for this workflow context; otherwise Resend tests will fail.
Would you confirm the secret is configured in repo or org settings?
src/Utopia/Messaging/Adapter/Email/Resend.php (2)
26-29: No changes needed—100-recipient limit verified as correct.Resend's Batch Emails API allows up to 100 emails per request, confirming the implementation in
getMaxMessagesPerRequest()is accurate and aligns with current API documentation.
112-136: Code correctly implements Resend batch API contract — no changes needed.The implementation aligns with Resend's documented API behavior. HTTP 200 returns a successful 2xx response, and per-item failures are returned in an errors array with each error object including a 0-based index that maps to the submitted array index. The code properly handles both cases: extracting error indices, matching them to recipients, and counting successful deliveries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
.env.dev (1)
4-5: Fix alphabetical ordering of environment variables.The dotenv keys are out of alphabetical order. Both
RESEND_API_KEYandRESEND_TEST_EMAILshould appear beforeSENDGRID_API_KEYto satisfy the linter.
🧹 Nitpick comments (2)
tests/Messaging/Adapter/Email/ResendTest.php (2)
110-114: Consider simplifying the assertions.The custom assertions are more verbose than necessary and repeat similar checks. You could either:
- Loop through the results array
- Extract a helper method for multi-recipient assertions
Example with a loop:
- $this->assertEquals(2, $response['deliveredTo'], \var_export($response, true)); - $this->assertEquals('', $response['results'][0]['error'], \var_export($response, true)); - $this->assertEquals('success', $response['results'][0]['status'], \var_export($response, true)); - $this->assertEquals('', $response['results'][1]['error'], \var_export($response, true)); - $this->assertEquals('success', $response['results'][1]['status'], \var_export($response, true)); + $this->assertEquals(2, $response['deliveredTo'], \var_export($response, true)); + foreach ($response['results'] as $result) { + $this->assertEquals('', $result['error'], \var_export($response, true)); + $this->assertEquals('success', $result['status'], \var_export($response, true)); + }
12-144: Reduce code duplication by extracting common setup.All test methods repeat the same setup code for retrieving the API key and creating the Resend sender instance. Consider extracting this to a
setUp()method to follow the DRY principle.Example refactor:
private Resend $sender; private string $testEmail; protected function setUp(): void { parent::setUp(); $key = \getenv('RESEND_API_KEY'); $this->sender = new Resend($key); $this->testEmail = \getenv('RESEND_TEST_EMAIL'); }Then each test method can use
$this->senderand$this->testEmaildirectly without repeating the setup.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
.env.dev(1 hunks).github/workflows/test.yml(2 hunks)docker-compose.yml(1 hunks)tests/Messaging/Adapter/Email/ResendTest.php(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- docker-compose.yml
- .github/workflows/test.yml
🧰 Additional context used
🧬 Code graph analysis (1)
tests/Messaging/Adapter/Email/ResendTest.php (3)
src/Utopia/Messaging/Adapter/Email/Resend.php (1)
Resend(9-167)src/Utopia/Messaging/Messages/Email/Attachment.php (1)
Attachment(5-33)tests/Messaging/Adapter/Base.php (1)
assertResponse(41-46)
🪛 dotenv-linter (4.0.0)
.env.dev
[warning] 4-4: [UnorderedKey] The RESEND_API_KEY key should go before the SENDGRID_API_KEY key
(UnorderedKey)
[warning] 5-5: [UnorderedKey] The RESEND_TEST_EMAIL key should go before the SENDGRID_API_KEY key
(UnorderedKey)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
.github/workflows/test.yml (1)
55-55: Consider using secrets for consistency.
RESEND_TEST_EMAILusesvarswhile other test email variables (lines 38-42) usesecrets. For consistency and to align with the established pattern in this workflow, consider storingRESEND_TEST_EMAILas a secret instead.- RESEND_TEST_EMAIL: ${{ vars.RESEND_TEST_EMAIL }} + RESEND_TEST_EMAIL: ${{ secrets.RESEND_TEST_EMAIL }}tests/Messaging/Adapter/Email/ResendTest.php (1)
15-21: Consider validating environment variables.If
RESEND_API_KEYorRESEND_TEST_EMAILaren't set,getenv()returnsfalse, which can lead to confusing test failures. Consider validating these are set or skipping tests gracefully if the configuration is missing.protected function setUp(): void { parent::setUp(); $key = \getenv('RESEND_API_KEY'); + if (empty($key)) { + $this->markTestSkipped('RESEND_API_KEY not configured'); + } $this->sender = new Resend($key); $this->testEmail = \getenv('RESEND_TEST_EMAIL'); + if (empty($this->testEmail)) { + $this->markTestSkipped('RESEND_TEST_EMAIL not configured'); + } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
.env.dev(1 hunks).github/workflows/test.yml(2 hunks)docker-compose.yml(1 hunks)tests/Messaging/Adapter/Email/ResendTest.php(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- docker-compose.yml
- .env.dev
🧰 Additional context used
🧬 Code graph analysis (1)
tests/Messaging/Adapter/Email/ResendTest.php (4)
src/Utopia/Messaging/Adapter.php (2)
Adapter(8-285)send(45-58)src/Utopia/Messaging/Adapter/Email/Resend.php (1)
Resend(9-167)src/Utopia/Messaging/Messages/Email/Attachment.php (1)
Attachment(5-33)tests/Messaging/Adapter/Base.php (1)
assertResponse(41-46)
🔇 Additional comments (5)
tests/Messaging/Adapter/Email/ResendTest.php (5)
23-45: LGTM!The test correctly validates basic email functionality including CC and BCC recipients.
47-66: LGTM!The test correctly validates HTML email functionality.
68-89: LGTM!The test correctly validates reply-to functionality.
91-114: LGTM!The test correctly validates batch email sending functionality, ensuring both messages are tracked individually in the response.
116-140: LGTM!The test correctly validates that Resend rejects attachments with an appropriate exception. The attachment file path doesn't need to exist since the exception is thrown before file processing.
Summary
Changes
Resendadapter class supporting basic email functionalityRESEND_API_KEYSummary by CodeRabbit
New Features
Documentation
Tests
Chores