Skip to content

Commit 0a50d87

Browse files
committed
feat: add Resend email adapter
Add support for Resend email service with a new adapter implementation and comprehensive tests.
1 parent 0b866d5 commit 0a50d87

6 files changed

Lines changed: 320 additions & 0 deletions

File tree

.env.dev

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
MAILGUN_API_KEY=
22
MAILGUN_DOMAIN=
33
SENDGRID_API_KEY=
4+
RESEND_API_KEY=
45
FCM_SERVICE_ACCOUNT_JSON=
56
FCM_TO=
67
TWILIO_ACCOUNT_SID=

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }}
1818
MAILGUN_DOMAIN: ${{ secrets.MAILGUN_DOMAIN }}
1919
SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
20+
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
2021
FCM_SERVICE_ACCOUNT_JSON: ${{ secrets.FCM_SERVICE_ACCOUNT_JSON }}
2122
FCM_TO: ${{ secrets.FCM_TO }}
2223
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ composer require utopia-php/messaging
2323
use \Utopia\Messaging\Messages\Email;
2424
use \Utopia\Messaging\Adapter\Email\SendGrid;
2525
use \Utopia\Messaging\Adapter\Email\Mailgun;
26+
use \Utopia\Messaging\Adapter\Email\Resend;
2627

2728
$message = new Email(
2829
to: ['team@appwrite.io'],
@@ -35,6 +36,9 @@ $messaging->send($message);
3536

3637
$messaging = new Mailgun('YOUR_API_KEY', 'YOUR_DOMAIN');
3738
$messaging->send($message);
39+
40+
$messaging = new Resend('YOUR_API_KEY');
41+
$messaging->send($message);
3842
```
3943

4044
## SMS
@@ -82,6 +86,7 @@ $messaging->send($message);
8286
### Email
8387
- [x] [SendGrid](https://sendgrid.com/)
8488
- [x] [Mailgun](https://www.mailgun.com/)
89+
- [x] [Resend](https://resend.com/)
8590
- [ ] [Mailjet](https://www.mailjet.com/)
8691
- [ ] [Mailchimp](https://www.mailchimp.com/)
8792
- [ ] [Postmark](https://postmarkapp.com/)

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ services:
1010
- MAILGUN_API_KEY
1111
- MAILGUN_DOMAIN
1212
- SENDGRID_API_KEY
13+
- RESEND_API_KEY
1314
- FCM_SERVICE_ACCOUNT_JSON
1415
- FCM_TO
1516
- TWILIO_ACCOUNT_SID
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php
2+
3+
namespace Utopia\Messaging\Adapter\Email;
4+
5+
use Utopia\Messaging\Adapter\Email as EmailAdapter;
6+
use Utopia\Messaging\Messages\Email as EmailMessage;
7+
use Utopia\Messaging\Response;
8+
9+
class Resend extends EmailAdapter
10+
{
11+
protected const NAME = 'Resend';
12+
13+
/**
14+
* @param string $apiKey Your Resend API key to authenticate with the API.
15+
*/
16+
public function __construct(
17+
private string $apiKey
18+
) {
19+
}
20+
21+
public function getName(): string
22+
{
23+
return static::NAME;
24+
}
25+
26+
public function getMaxMessagesPerRequest(): int
27+
{
28+
return 100;
29+
}
30+
31+
/**
32+
* Uses Resend's batch sending API to send multiple emails at once.
33+
*
34+
* @link https://resend.com/docs/api-reference/emails/send-batch-emails
35+
*/
36+
protected function process(EmailMessage $message): array
37+
{
38+
// Resend doesn't support attachments yet
39+
if (! \is_null($message->getAttachments()) && ! empty($message->getAttachments())) {
40+
throw new \Exception('Resend does not support attachments at this time');
41+
}
42+
43+
$response = new Response($this->getType());
44+
45+
$emails = [];
46+
foreach ($message->getTo() as $to) {
47+
$email = [
48+
'from' => $message->getFromName()
49+
? "{$message->getFromName()} <{$message->getFromEmail()}>"
50+
: $message->getFromEmail(),
51+
'to' => [$to],
52+
'subject' => $message->getSubject(),
53+
];
54+
55+
if ($message->isHtml()) {
56+
$email['html'] = $message->getContent();
57+
} else {
58+
$email['text'] = $message->getContent();
59+
}
60+
61+
if (! empty($message->getReplyToEmail())) {
62+
$email['reply_to'] = $message->getReplyToName()
63+
? ["{$message->getReplyToName()} <{$message->getReplyToEmail()}>"]
64+
: [$message->getReplyToEmail()];
65+
}
66+
67+
if (! \is_null($message->getCC()) && ! empty($message->getCC())) {
68+
$ccList = [];
69+
foreach ($message->getCC() as $cc) {
70+
if (! empty($cc['email'])) {
71+
$ccList[] = ! empty($cc['name'])
72+
? "{$cc['name']} <{$cc['email']}>"
73+
: $cc['email'];
74+
}
75+
}
76+
if (! empty($ccList)) {
77+
$email['cc'] = $ccList;
78+
}
79+
}
80+
81+
if (! \is_null($message->getBCC()) && ! empty($message->getBCC())) {
82+
$bccList = [];
83+
foreach ($message->getBCC() as $bcc) {
84+
if (! empty($bcc['email'])) {
85+
$bccList[] = ! empty($bcc['name'])
86+
? "{$bcc['name']} <{$bcc['email']}>"
87+
: $bcc['email'];
88+
}
89+
}
90+
if (! empty($bccList)) {
91+
$email['bcc'] = $bccList;
92+
}
93+
}
94+
95+
$emails[] = $email;
96+
}
97+
98+
$headers = [
99+
'Authorization: Bearer '.$this->apiKey,
100+
'Content-Type: application/json',
101+
];
102+
103+
$result = $this->request(
104+
method: 'POST',
105+
url: 'https://api.resend.com/emails/batch',
106+
headers: $headers,
107+
body: $emails, // @phpstan-ignore-line
108+
);
109+
110+
$statusCode = $result['statusCode'];
111+
112+
if ($statusCode === 200) {
113+
$responseData = $result['response'];
114+
115+
if (isset($responseData['errors']) && ! empty($responseData['errors'])) {
116+
$failedIndices = [];
117+
foreach ($responseData['errors'] as $error) {
118+
$failedIndices[$error['index']] = $error['message'];
119+
}
120+
121+
foreach ($message->getTo() as $index => $to) {
122+
if (isset($failedIndices[$index])) {
123+
$response->addResult($to, $failedIndices[$index]);
124+
} else {
125+
$response->addResult($to);
126+
}
127+
}
128+
129+
$successCount = \count($message->getTo()) - \count($failedIndices);
130+
$response->setDeliveredTo($successCount);
131+
} else {
132+
$response->setDeliveredTo(\count($message->getTo()));
133+
foreach ($message->getTo() as $to) {
134+
$response->addResult($to);
135+
}
136+
}
137+
} elseif ($statusCode >= 400 && $statusCode < 500) {
138+
$errorMessage = 'Unknown error';
139+
140+
if (\is_string($result['response'])) {
141+
$errorMessage = $result['response'];
142+
} elseif (isset($result['response']['message'])) {
143+
$errorMessage = $result['response']['message'];
144+
} elseif (isset($result['response']['error'])) {
145+
$errorMessage = $result['response']['error'];
146+
}
147+
148+
foreach ($message->getTo() as $to) {
149+
$response->addResult($to, $errorMessage);
150+
}
151+
} elseif ($statusCode >= 500) {
152+
$errorMessage = 'Server error';
153+
154+
if (\is_string($result['response'])) {
155+
$errorMessage = $result['response'];
156+
} elseif (isset($result['response']['message'])) {
157+
$errorMessage = $result['response']['message'];
158+
}
159+
160+
foreach ($message->getTo() as $to) {
161+
$response->addResult($to, $errorMessage);
162+
}
163+
}
164+
165+
return $response->toArray();
166+
}
167+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace Utopia\Tests\Adapter\Email;
4+
5+
use Utopia\Messaging\Adapter\Email\Resend;
6+
use Utopia\Messaging\Messages\Email;
7+
use Utopia\Messaging\Messages\Email\Attachment;
8+
use Utopia\Tests\Adapter\Base;
9+
10+
class ResendTest extends Base
11+
{
12+
public function testSendEmail(): void
13+
{
14+
$key = \getenv('RESEND_API_KEY');
15+
$sender = new Resend($key);
16+
17+
$to = \getenv('TEST_EMAIL');
18+
$subject = 'Test Subject';
19+
$content = 'Test Content';
20+
$fromEmail = \getenv('TEST_FROM_EMAIL');
21+
$cc = [['email' => \getenv('TEST_CC_EMAIL')]];
22+
$bcc = [['name' => \getenv('TEST_BCC_NAME'), 'email' => \getenv('TEST_BCC_EMAIL')]];
23+
24+
$message = new Email(
25+
to: [$to],
26+
subject: $subject,
27+
content: $content,
28+
fromName: 'Test Sender',
29+
fromEmail: $fromEmail,
30+
cc: $cc,
31+
bcc: $bcc,
32+
);
33+
34+
$response = $sender->send($message);
35+
36+
$this->assertResponse($response);
37+
}
38+
39+
public function testSendEmailWithHtml(): void
40+
{
41+
$key = \getenv('RESEND_API_KEY');
42+
$sender = new Resend($key);
43+
44+
$to = \getenv('TEST_EMAIL');
45+
$subject = 'Test HTML Subject';
46+
$content = '<h1>Test HTML Content</h1><p>This is a test email with HTML content.</p>';
47+
$fromEmail = \getenv('TEST_FROM_EMAIL');
48+
49+
$message = new Email(
50+
to: [$to],
51+
subject: $subject,
52+
content: $content,
53+
fromName: 'Test Sender',
54+
fromEmail: $fromEmail,
55+
html: true,
56+
);
57+
58+
$response = $sender->send($message);
59+
60+
$this->assertResponse($response);
61+
}
62+
63+
public function testSendEmailWithReplyTo(): void
64+
{
65+
$key = \getenv('RESEND_API_KEY');
66+
$sender = new Resend($key);
67+
68+
$to = \getenv('TEST_EMAIL');
69+
$subject = 'Test Reply-To Subject';
70+
$content = 'Test Content with Reply-To';
71+
$fromEmail = \getenv('TEST_FROM_EMAIL');
72+
$replyToEmail = \getenv('TEST_CC_EMAIL');
73+
74+
$message = new Email(
75+
to: [$to],
76+
subject: $subject,
77+
content: $content,
78+
fromName: 'Test Sender',
79+
fromEmail: $fromEmail,
80+
replyToName: 'Reply To Name',
81+
replyToEmail: $replyToEmail,
82+
);
83+
84+
$response = $sender->send($message);
85+
86+
$this->assertResponse($response);
87+
}
88+
89+
public function testSendMultipleEmails(): void
90+
{
91+
$key = \getenv('RESEND_API_KEY');
92+
$sender = new Resend($key);
93+
94+
$to1 = \getenv('TEST_EMAIL');
95+
$to2 = \getenv('TEST_CC_EMAIL');
96+
$subject = 'Test Batch Subject';
97+
$content = 'Test Batch Content';
98+
$fromEmail = \getenv('TEST_FROM_EMAIL');
99+
100+
$message = new Email(
101+
to: [$to1, $to2],
102+
subject: $subject,
103+
content: $content,
104+
fromName: 'Test Sender',
105+
fromEmail: $fromEmail,
106+
);
107+
108+
$response = $sender->send($message);
109+
110+
$this->assertEquals(2, $response['deliveredTo'], \var_export($response, true));
111+
$this->assertEquals('', $response['results'][0]['error'], \var_export($response, true));
112+
$this->assertEquals('success', $response['results'][0]['status'], \var_export($response, true));
113+
$this->assertEquals('', $response['results'][1]['error'], \var_export($response, true));
114+
$this->assertEquals('success', $response['results'][1]['status'], \var_export($response, true));
115+
}
116+
117+
public function testSendEmailWithAttachmentsThrowsException(): void
118+
{
119+
$this->expectException(\Exception::class);
120+
$this->expectExceptionMessage('Resend does not support attachments at this time');
121+
122+
$key = \getenv('RESEND_API_KEY');
123+
$sender = new Resend($key);
124+
125+
$to = \getenv('TEST_EMAIL');
126+
$subject = 'Test Subject';
127+
$content = 'Test Content';
128+
$fromEmail = \getenv('TEST_FROM_EMAIL');
129+
130+
$message = new Email(
131+
to: [$to],
132+
subject: $subject,
133+
content: $content,
134+
fromName: 'Test Sender',
135+
fromEmail: $fromEmail,
136+
attachments: [new Attachment(
137+
name: 'image.png',
138+
path: __DIR__.'/../../../assets/image.png',
139+
type: 'image/png'
140+
)],
141+
);
142+
143+
$sender->send($message);
144+
}
145+
}

0 commit comments

Comments
 (0)