Skip to content

Conversation

@ChiragAgg5k
Copy link
Member

@ChiragAgg5k ChiragAgg5k commented Oct 21, 2025

Summary

  • Add Resend email adapter implementation
  • Add comprehensive test coverage for the Resend adapter
  • Update documentation and configuration files

Changes

  • New Resend adapter class supporting basic email functionality
  • Environment configuration for RESEND_API_KEY
  • Updated README with Resend usage examples

Summary by CodeRabbit

  • New Features

    • Added Resend email provider: send plain text and HTML emails, reply-to, multiple recipients, CC/BCC; batch sends up to 100 messages. Attachments not supported.
  • Documentation

    • README updated with Resend usage examples.
  • Tests

    • New test suite covering sending scenarios and attachment rejection.
  • Chores

    • Added RESEND_API_KEY and RESEND_TEST_EMAIL to env, CI, and test configs.

Add support for Resend email service with a new adapter implementation and comprehensive tests.
@coderabbitai
Copy link

coderabbitai bot commented Oct 21, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Adds 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)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: add Resend email adapter" clearly and accurately describes the primary change in this pull request. The main objective is to introduce a new Resend email adapter class (src/Utopia/Messaging/Adapter/Email/Resend.php) that enables email sending via the Resend service. While the changeset includes supporting modifications such as environment variable configuration, workflow updates, test coverage, and documentation, these are all supplementary to the core feature. The title is specific, concise, and directly communicates the essential change without ambiguity or vague terminology.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-resend-email-adapter

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9704e0c and 1ba18f1.

📒 Files selected for processing (1)
  • tests/Messaging/Adapter/Email/ResendTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Messaging/Adapter/Email/ResendTest.php

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a 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 Resend adapter 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.

Copy link

@coderabbitai coderabbitai bot left a 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: Add Accept: application/json header.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0b866d5 and 0a50d87.

📒 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.

Copy link

@coderabbitai coderabbitai bot left a 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_KEY and RESEND_TEST_EMAIL should appear before SENDGRID_API_KEY to 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:

  1. Loop through the results array
  2. 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->sender and $this->testEmail directly without repeating the setup.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a50d87 and 200a2b8.

📒 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)

Copy link

@coderabbitai coderabbitai bot left a 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_EMAIL uses vars while other test email variables (lines 38-42) use secrets. For consistency and to align with the established pattern in this workflow, consider storing RESEND_TEST_EMAIL as 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_KEY or RESEND_TEST_EMAIL aren't set, getenv() returns false, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 200a2b8 and 9704e0c.

📒 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.

@abnegate abnegate merged commit 6c5be45 into main Oct 22, 2025
3 of 4 checks passed
@abnegate abnegate deleted the feat-resend-email-adapter branch October 22, 2025 04:27
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