Skip to content

Commit b590728

Browse files
authored
Merge pull request #28 from danielhe4rt/main
feat: add resend provider
2 parents 7e4899e + 873a8b2 commit b590728

File tree

6 files changed

+357
-9
lines changed

6 files changed

+357
-9
lines changed

src/Drivers/ResendDriver.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace Backstage\Mails\Drivers;
4+
5+
use Backstage\Mails\Contracts\MailDriverContract;
6+
use Backstage\Mails\Enums\EventType;
7+
use Illuminate\Mail\Events\MessageSending;
8+
9+
class ResendDriver extends MailDriver implements MailDriverContract
10+
{
11+
public function registerWebhooks($components): void
12+
{
13+
$components->warn("Resend doesn't allow registering webhooks via the API. ");
14+
$components->info("Please register your webhooks manually in the Resend dashboard.");
15+
}
16+
17+
public function verifyWebhookSignature(array $payload): bool
18+
{
19+
return true;
20+
}
21+
22+
public function getUuidFromPayload(array $payload): ?string
23+
{
24+
return collect($payload['data']['headers'])
25+
->where('name', config('mails.headers.uuid'))
26+
->first()['value'] ?? null;
27+
}
28+
29+
protected function getTimestampFromPayload(array $payload): string
30+
{
31+
return $payload['data']['created_at'] ?? now();
32+
}
33+
34+
public function eventMapping(): array
35+
{
36+
return [
37+
EventType::CLICKED->value => ['type' => 'email.clicked'],
38+
EventType::COMPLAINED->value => ['type' => 'email.complained'],
39+
EventType::DELIVERED->value => ['type' => 'email.delivered'],
40+
EventType::HARD_BOUNCED->value => ['type' => 'email.bounced'],
41+
EventType::OPENED->value => ['type' => 'email.opened'],
42+
EventType::SOFT_BOUNCED->value => ['type' => 'email.delivery_delayed'],
43+
];
44+
}
45+
46+
public function dataMapping(): array
47+
{
48+
return [
49+
'ip_address' => 'data.click.ipAddress',
50+
'link' => 'data.click.link',
51+
'user_agent' => 'data.click.userAgent',
52+
];
53+
}
54+
55+
public function attachUuidToMail(MessageSending $event, string $uuid): MessageSending
56+
{
57+
$event->message->getHeaders()->addTextHeader(config('mails.headers.uuid'), $uuid);
58+
59+
return $event;
60+
}
61+
}

src/Enums/Provider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ enum Provider: string
66
{
77
case POSTMARK = 'postmark';
88
case MAILGUN = 'mailgun';
9+
case RESEND = 'resend';
910
}

src/Managers/MailProviderManager.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Support\Manager;
66
use Backstage\Mails\Drivers\MailgunDriver;
77
use Backstage\Mails\Drivers\PostmarkDriver;
8+
use Backstage\Mails\Drivers\ResendDriver;
89

910
class MailProviderManager extends Manager
1011
{
@@ -23,6 +24,11 @@ protected function createMailgunDriver(): MailgunDriver
2324
return new MailgunDriver;
2425
}
2526

27+
protected function createResendDriver(): ResendDriver
28+
{
29+
return new ResendDriver;
30+
}
31+
2632
public function getDefaultDriver(): ?string
2733
{
2834
return null;

tests/Pest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<?php
22

3+
use Illuminate\Foundation\Testing\RefreshDatabase;
34
use Illuminate\Support\Facades\Mail;
45
use Backstage\Mails\Tests\TestCase;
56

67
uses(TestCase::class)
8+
->use(RefreshDatabase::class)
79
->in(__DIR__);
810

911
beforeEach(function () {

tests/ResendTest.php

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<?php
2+
3+
use Backstage\Mails\Enums\EventType;
4+
use Backstage\Mails\Models\Mail as MailModel;
5+
use Backstage\Mails\Models\MailEvent;
6+
use Illuminate\Mail\Message;
7+
use Illuminate\Support\Facades\Mail;
8+
use Illuminate\Support\Facades\URL;
9+
use function Pest\Laravel\assertDatabaseHas;
10+
use function Pest\Laravel\post;
11+
12+
it('can receive incoming delivery webhook from resend', function () {
13+
Mail::send([], [], function (Message $message) {
14+
$message->to('[email protected]')
15+
16+
17+
18+
->subject('Test')
19+
->text('Text')
20+
->html('<p>HTML</p>');
21+
});
22+
23+
$mail = MailModel::latest()->first();
24+
25+
post(URL::signedRoute('mails.webhook', ['provider' => 'resend']), [
26+
'created_at' => '2023-05-19T22:09:32Z',
27+
'data' => [
28+
'created_at' => '2025-01-09 14:17:29.059104+00',
29+
'email_id' => 'dummy-id',
30+
'headers' => [
31+
[
32+
'name' => config('mails.headers.uuid'),
33+
'value' => $mail->uuid
34+
]
35+
],
36+
'from' => '[email protected]',
37+
'subject' => 'Test',
38+
'to' => ['[email protected]'],
39+
'cc' => ['[email protected]'],
40+
'bcc' => ['[email protected]'],
41+
],
42+
'type' => 'email.delivered',
43+
])->assertAccepted();
44+
45+
assertDatabaseHas((new MailEvent)->getTable(), [
46+
'type' => EventType::DELIVERED->value,
47+
]);
48+
});
49+
50+
it('can receive incoming hard bounce webhook from resend', function () {
51+
Mail::send([], [], function (Message $message) {
52+
$message->to('[email protected]')
53+
54+
55+
56+
->subject('Test')
57+
->text('Text')
58+
->html('<p>HTML</p>');
59+
});
60+
61+
$mail = MailModel::latest()->first();
62+
63+
post(URL::signedRoute('mails.webhook', ['provider' => 'resend']), [
64+
'created_at' => '2023-05-19T22:09:32Z',
65+
'data' => [
66+
'created_at' => '2025-01-09 14:17:29.059104+00',
67+
'email_id' => 'dummy-id',
68+
'from' => '[email protected]',
69+
'headers' => [
70+
[
71+
'name' => config('mails.headers.uuid'),
72+
'value' => $mail->uuid
73+
]
74+
],
75+
'subject' => 'Test',
76+
'to' => ['[email protected]'],
77+
'cc' => ['[email protected]'],
78+
'bcc' => ['[email protected]'],
79+
],
80+
'type' => 'email.bounced',
81+
])->assertAccepted();
82+
83+
assertDatabaseHas((new MailEvent)->getTable(), [
84+
'type' => EventType::HARD_BOUNCED->value,
85+
]);
86+
});
87+
88+
it('can receive incoming soft bounce webhook from resend', function () {
89+
Mail::send([], [], function (Message $message) {
90+
$message->to('[email protected]')
91+
92+
93+
94+
->subject('Test')
95+
->text('Text')
96+
->html('<p>HTML</p>');
97+
});
98+
99+
$mail = MailModel::latest()->first();
100+
101+
post(URL::signedRoute('mails.webhook', ['provider' => 'resend']), [
102+
'created_at' => '2023-05-19T22:09:32Z',
103+
'data' => [
104+
'created_at' => '2025-01-09 14:17:29.059104+00',
105+
'email_id' => 'dummy-id',
106+
'headers' => [
107+
[
108+
'name' => config('mails.headers.uuid'),
109+
'value' => $mail->uuid
110+
]
111+
],
112+
'from' => '[email protected]',
113+
'subject' => 'Test',
114+
'to' => ['[email protected]'],
115+
'cc' => ['[email protected]'],
116+
'bcc' => ['[email protected]'],
117+
],
118+
'type' => 'email.delivery_delayed',
119+
])->assertAccepted();
120+
121+
assertDatabaseHas((new MailEvent)->getTable(), [
122+
'type' => EventType::SOFT_BOUNCED->value,
123+
]);
124+
});
125+
126+
it('can receive incoming complaint webhook from resend', function () {
127+
Mail::send([], [], function (Message $message) {
128+
$message->to('[email protected]')
129+
130+
131+
132+
->subject('Test')
133+
->text('Text')
134+
->html('<p>HTML</p>');
135+
});
136+
137+
$mail = MailModel::latest()->first();
138+
139+
post(URL::signedRoute('mails.webhook', ['provider' => 'resend']), [
140+
'created_at' => '2023-05-19T22:09:32Z',
141+
'data' => [
142+
'created_at' => '2025-01-09 14:17:29.059104+00',
143+
'email_id' => 'dummy-id',
144+
'headers' => [
145+
[
146+
'name' => config('mails.headers.uuid'),
147+
'value' => $mail->uuid
148+
]
149+
],
150+
'from' => '[email protected]',
151+
'subject' => 'Test',
152+
'to' => ['[email protected]'],
153+
'cc' => ['[email protected]'],
154+
'bcc' => ['[email protected]'],
155+
],
156+
'type' => 'email.complained',
157+
])->assertAccepted();
158+
159+
assertDatabaseHas((new MailEvent)->getTable(), [
160+
'type' => EventType::COMPLAINED->value,
161+
]);
162+
});
163+
164+
it('can receive incoming open webhook from resend', function () {
165+
Mail::send([], [], function (Message $message) {
166+
$message->to('[email protected]')
167+
168+
169+
170+
->subject('Test')
171+
->text('Text')
172+
->html('<p>HTML</p>');
173+
});
174+
175+
$mail = MailModel::latest()->first();
176+
177+
post(URL::signedRoute('mails.webhook', ['provider' => 'resend']), [
178+
'created_at' => '2023-05-19T22:09:32Z',
179+
'data' => [
180+
'created_at' => '2025-01-09 14:17:29.059104+00',
181+
'email_id' => 'dummy-id',
182+
'from' => '[email protected]',
183+
'headers' => [
184+
[
185+
'name' => config('mails.headers.uuid'),
186+
'value' => $mail->uuid
187+
]
188+
],
189+
'subject' => 'Test',
190+
'to' => ['[email protected]'],
191+
'cc' => ['[email protected]'],
192+
'bcc' => ['[email protected]'],
193+
],
194+
'type' => 'email.opened',
195+
])->assertAccepted();
196+
197+
assertDatabaseHas((new MailEvent)->getTable(), [
198+
'type' => EventType::OPENED->value,
199+
]);
200+
});
201+
202+
it('can receive incoming click webhook from resend', function () {
203+
Mail::send([], [], function (Message $message) {
204+
$message->to('[email protected]')
205+
206+
207+
208+
->subject('Test')
209+
->text('Text')
210+
->html('<p>HTML</p>');
211+
});
212+
213+
$mail = MailModel::latest()->first();
214+
215+
post(URL::signedRoute('mails.webhook', ['provider' => 'resend']), [
216+
'created_at' => '2023-05-19T22:09:32Z',
217+
'data' => [
218+
'created_at' => '2025-01-09 14:17:29.059104+00',
219+
'email_id' => 'dummy-id',
220+
'from' => '[email protected]',
221+
'subject' => 'Test',
222+
'headers' => [
223+
[
224+
'name' => config('mails.headers.uuid'),
225+
'value' => $mail->uuid
226+
]
227+
],
228+
'to' => ['[email protected]'],
229+
'cc' => ['[email protected]'],
230+
'bcc' => ['[email protected]'],
231+
'click' => [
232+
'ipAddress' => '122.115.53.11',
233+
'link' => 'https://resend.com',
234+
'timestamp' => '2024-11-24T05:00:57.163Z',
235+
'userAgent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
236+
],
237+
],
238+
'type' => 'email.clicked',
239+
])->assertAccepted();
240+
241+
assertDatabaseHas((new MailEvent)->getTable(), [
242+
'type' => EventType::CLICKED->value,
243+
'link' => 'https://resend.com',
244+
]);
245+
});

0 commit comments

Comments
 (0)