Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.dev
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MAILGUN_API_KEY=
MAILGUN_DOMAIN=
SENDGRID_API_KEY=
RESEND_API_KEY=
FCM_SERVICE_ACCOUNT_JSON=
FCM_TO=
TWILIO_ACCOUNT_SID=
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }}
MAILGUN_DOMAIN: ${{ secrets.MAILGUN_DOMAIN }}
SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
FCM_SERVICE_ACCOUNT_JSON: ${{ secrets.FCM_SERVICE_ACCOUNT_JSON }}
FCM_TO: ${{ secrets.FCM_TO }}
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ composer require utopia-php/messaging
use \Utopia\Messaging\Messages\Email;
use \Utopia\Messaging\Adapter\Email\SendGrid;
use \Utopia\Messaging\Adapter\Email\Mailgun;
use \Utopia\Messaging\Adapter\Email\Resend;

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

$messaging = new Mailgun('YOUR_API_KEY', 'YOUR_DOMAIN');
$messaging->send($message);

$messaging = new Resend('YOUR_API_KEY');
$messaging->send($message);
```

## SMS
Expand Down Expand Up @@ -82,6 +86,7 @@ $messaging->send($message);
### Email
- [x] [SendGrid](https://sendgrid.com/)
- [x] [Mailgun](https://www.mailgun.com/)
- [x] [Resend](https://resend.com/)
- [ ] [Mailjet](https://www.mailjet.com/)
- [ ] [Mailchimp](https://www.mailchimp.com/)
- [ ] [Postmark](https://postmarkapp.com/)
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ services:
- MAILGUN_API_KEY
- MAILGUN_DOMAIN
- SENDGRID_API_KEY
- RESEND_API_KEY
- FCM_SERVICE_ACCOUNT_JSON
- FCM_TO
- TWILIO_ACCOUNT_SID
Expand Down
167 changes: 167 additions & 0 deletions src/Utopia/Messaging/Adapter/Email/Resend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

namespace Utopia\Messaging\Adapter\Email;

use Utopia\Messaging\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Messages\Email as EmailMessage;
use Utopia\Messaging\Response;

class Resend extends EmailAdapter
{
protected const NAME = 'Resend';

/**
* @param string $apiKey Your Resend API key to authenticate with the API.
*/
public function __construct(
private string $apiKey
) {
}

public function getName(): string
{
return static::NAME;
}

public function getMaxMessagesPerRequest(): int
{
return 100;
}

/**
* Uses Resend's batch sending API to send multiple emails at once.
*
* @link https://resend.com/docs/api-reference/emails/send-batch-emails
*/
protected function process(EmailMessage $message): array
{
// Resend doesn't support attachments yet
if (! \is_null($message->getAttachments()) && ! empty($message->getAttachments())) {
throw new \Exception('Resend does not support attachments at this time');
}

$response = new Response($this->getType());

$emails = [];
foreach ($message->getTo() as $to) {
$email = [
'from' => $message->getFromName()
? "{$message->getFromName()} <{$message->getFromEmail()}>"
: $message->getFromEmail(),
'to' => [$to],
'subject' => $message->getSubject(),
];

if ($message->isHtml()) {
$email['html'] = $message->getContent();
} else {
$email['text'] = $message->getContent();
}

if (! empty($message->getReplyToEmail())) {
$email['reply_to'] = $message->getReplyToName()
? ["{$message->getReplyToName()} <{$message->getReplyToEmail()}>"]
: [$message->getReplyToEmail()];
}

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'];
}
}
if (! empty($ccList)) {
$email['cc'] = $ccList;
}
}

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'];
}
}
if (! empty($bccList)) {
$email['bcc'] = $bccList;
}
}

$emails[] = $email;
}

$headers = [
'Authorization: Bearer '.$this->apiKey,
'Content-Type: application/json',
];

$result = $this->request(
method: 'POST',
url: 'https://api.resend.com/emails/batch',
headers: $headers,
body: $emails, // @phpstan-ignore-line
);

$statusCode = $result['statusCode'];

if ($statusCode === 200) {
$responseData = $result['response'];

if (isset($responseData['errors']) && ! empty($responseData['errors'])) {
$failedIndices = [];
foreach ($responseData['errors'] as $error) {
$failedIndices[$error['index']] = $error['message'];
}

foreach ($message->getTo() as $index => $to) {
if (isset($failedIndices[$index])) {
$response->addResult($to, $failedIndices[$index]);
} else {
$response->addResult($to);
}
}

$successCount = \count($message->getTo()) - \count($failedIndices);
$response->setDeliveredTo($successCount);
} else {
$response->setDeliveredTo(\count($message->getTo()));
foreach ($message->getTo() as $to) {
$response->addResult($to);
}
}
} elseif ($statusCode >= 400 && $statusCode < 500) {
$errorMessage = 'Unknown error';

if (\is_string($result['response'])) {
$errorMessage = $result['response'];
} elseif (isset($result['response']['message'])) {
$errorMessage = $result['response']['message'];
} elseif (isset($result['response']['error'])) {
$errorMessage = $result['response']['error'];
}

foreach ($message->getTo() as $to) {
$response->addResult($to, $errorMessage);
}
} elseif ($statusCode >= 500) {
$errorMessage = 'Server error';

if (\is_string($result['response'])) {
$errorMessage = $result['response'];
} elseif (isset($result['response']['message'])) {
$errorMessage = $result['response']['message'];
}

foreach ($message->getTo() as $to) {
$response->addResult($to, $errorMessage);
}
}

return $response->toArray();
}
}
145 changes: 145 additions & 0 deletions tests/Messaging/Adapter/Email/ResendTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

namespace Utopia\Tests\Adapter\Email;

use Utopia\Messaging\Adapter\Email\Resend;
use Utopia\Messaging\Messages\Email;
use Utopia\Messaging\Messages\Email\Attachment;
use Utopia\Tests\Adapter\Base;

class ResendTest extends Base
{
public function testSendEmail(): void
{
$key = \getenv('RESEND_API_KEY');
$sender = new Resend($key);

$to = \getenv('TEST_EMAIL');
$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')]];

$message = new Email(
to: [$to],
subject: $subject,
content: $content,
fromName: 'Test Sender',
fromEmail: $fromEmail,
cc: $cc,
bcc: $bcc,
);

$response = $sender->send($message);

$this->assertResponse($response);
}

public function testSendEmailWithHtml(): void
{
$key = \getenv('RESEND_API_KEY');
$sender = new Resend($key);

$to = \getenv('TEST_EMAIL');
$subject = 'Test HTML Subject';
$content = '<h1>Test HTML Content</h1><p>This is a test email with HTML content.</p>';
$fromEmail = \getenv('TEST_FROM_EMAIL');

$message = new Email(
to: [$to],
subject: $subject,
content: $content,
fromName: 'Test Sender',
fromEmail: $fromEmail,
html: true,
);

$response = $sender->send($message);

$this->assertResponse($response);
}

public function testSendEmailWithReplyTo(): void
{
$key = \getenv('RESEND_API_KEY');
$sender = new Resend($key);

$to = \getenv('TEST_EMAIL');
$subject = 'Test Reply-To Subject';
$content = 'Test Content with Reply-To';
$fromEmail = \getenv('TEST_FROM_EMAIL');
$replyToEmail = \getenv('TEST_CC_EMAIL');

$message = new Email(
to: [$to],
subject: $subject,
content: $content,
fromName: 'Test Sender',
fromEmail: $fromEmail,
replyToName: 'Reply To Name',
replyToEmail: $replyToEmail,
);

$response = $sender->send($message);

$this->assertResponse($response);
}

public function testSendMultipleEmails(): void
{
$key = \getenv('RESEND_API_KEY');
$sender = new Resend($key);

$to1 = \getenv('TEST_EMAIL');
$to2 = \getenv('TEST_CC_EMAIL');
$subject = 'Test Batch Subject';
$content = 'Test Batch Content';
$fromEmail = \getenv('TEST_FROM_EMAIL');

$message = new Email(
to: [$to1, $to2],
subject: $subject,
content: $content,
fromName: 'Test Sender',
fromEmail: $fromEmail,
);

$response = $sender->send($message);

$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));
}

public function testSendEmailWithAttachmentsThrowsException(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Resend does not support attachments at this time');

$key = \getenv('RESEND_API_KEY');
$sender = new Resend($key);

$to = \getenv('TEST_EMAIL');
$subject = 'Test Subject';
$content = 'Test Content';
$fromEmail = \getenv('TEST_FROM_EMAIL');

$message = new Email(
to: [$to],
subject: $subject,
content: $content,
fromName: 'Test Sender',
fromEmail: $fromEmail,
attachments: [new Attachment(
name: 'image.png',
path: __DIR__.'/../../../assets/image.png',
type: 'image/png'
)],
);

$sender->send($message);
}
}
Loading