Skip to content

Commit a27b2f6

Browse files
authored
feat: support tags and idempotency keys (#104)
1 parent de4b144 commit a27b2f6

File tree

2 files changed

+85
-9
lines changed

2 files changed

+85
-9
lines changed

src/Transport/ResendTransportFactory.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Resend\Contracts\Client;
77
use Symfony\Component\Mailer\Envelope;
88
use Symfony\Component\Mailer\Exception\TransportException;
9+
use Symfony\Component\Mailer\Header\MetadataHeader;
910
use Symfony\Component\Mailer\SentMessage;
1011
use Symfony\Component\Mailer\Transport\AbstractTransport;
1112
use Symfony\Component\Mime\Address;
@@ -33,15 +34,28 @@ protected function doSend(SentMessage $message): void
3334
$envelope = $message->getEnvelope();
3435

3536
$headers = [];
36-
$headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender', 'reply-to'];
37+
$tags = [];
38+
$headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender', 'reply-to', 'resend-idempotency-key'];
3739
foreach ($email->getHeaders()->all() as $name => $header) {
40+
if ($header instanceof MetadataHeader) {
41+
$tags[] = ['name' => $header->getKey(), 'value' => $header->getValue()];
42+
43+
continue;
44+
}
45+
3846
if (in_array($name, $headersToBypass, true)) {
3947
continue;
4048
}
4149

4250
$headers[$header->getName()] = $header->getBodyAsString();
4351
}
4452

53+
$options = [];
54+
55+
if ($email->getHeaders()->has('Resend-Idempotency-Key')) {
56+
$options['idempotency_key'] = $email->getHeaders()->get('Resend-Idempotency-Key')->getBodyAsString();
57+
}
58+
4559
try {
4660
$result = $this->resend->emails->send([
4761
'bcc' => $this->stringifyAddresses($email->getBcc()),
@@ -51,10 +65,11 @@ protected function doSend(SentMessage $message): void
5165
'html' => $email->getHtmlBody(),
5266
'reply_to' => $this->stringifyAddresses($email->getReplyTo()),
5367
'subject' => $email->getSubject(),
68+
'tags' => $tags,
5469
'text' => $email->getTextBody(),
5570
'to' => $this->stringifyAddresses($this->getRecipients($email, $envelope)),
5671
'attachments' => $this->getAttachments($email),
57-
]);
72+
], $options);
5873
} catch (Exception $exception) {
5974
throw new TransportException(
6075
sprintf('Request to the Resend API failed. Reason: %s', $exception->getMessage()),

tests/Transport/ResendTransportFactory.php

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Resend\Laravel\Transport\ResendTransportFactory;
88
use Resend\Service\Email as EmailService;
99
use Symfony\Component\Mailer\Exception\TransportException;
10+
use Symfony\Component\Mailer\Header\MetadataHeader;
1011
use Symfony\Component\Mime\Address;
1112
use Symfony\Component\Mime\Email as SymfonyEmail;
1213

@@ -61,7 +62,7 @@
6162
$arg['cc'] === ['cc@example.com'] &&
6263
$arg['bcc'] === ['bcc@example.com'] &&
6364
$arg['reply_to'] === ['reply-to@example.com'];
64-
}))
65+
}), Mockery::type('array'))
6566
->andReturn($apiResponse);
6667

6768
$this->transporter->send($email);
@@ -88,7 +89,7 @@
8889
->with(Mockery::on(function ($arg) {
8990
return $arg['from'] === 'from@example.com' &&
9091
$arg['to'] === ['"Acme" <to@example.com>', '"Acme Sales" <sales@example.com>'];
91-
}))
92+
}), Mockery::type('array'))
9293
->andReturn($apiResponse);
9394

9495
$this->transporter->send($email);
@@ -114,7 +115,7 @@
114115
$arg['to'] === ['"Acme" <to@example.com>'] &&
115116
$arg['subject'] === 'Test Subject' &&
116117
array_key_exists('X-Entity-Ref-ID', $arg['headers']);
117-
}))
118+
}), Mockery::type('array'))
118119
->andReturn($apiResponse);
119120

120121
$this->transporter->send($email);
@@ -151,7 +152,7 @@
151152
$arg['attachments'][0]['filename'] === 'lorem-ipsum.txt' &&
152153
$arg['attachments'][0]['content'] === 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gQWVuZWFuIG51bmMgYXVndWUsIGNvbnNlY3RldHVyIGlkIG5lcXVlIGVnZXQsIHZhcml1cyBkaWduaXNzaW0gZGlhbS4=' &&
153154
$arg['attachments'][0]['content_type'] === 'text/plain';
154-
}))
155+
}), Mockery::type('array'))
155156
->andReturn($apiResponse);
156157

157158
$this->transporter->send($email);
@@ -190,7 +191,7 @@
190191
$arg['attachments'][0]['content'] === 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gQWVuZWFuIG51bmMgYXVndWUsIGNvbnNlY3RldHVyIGlkIG5lcXVlIGVnZXQsIHZhcml1cyBkaWduaXNzaW0gZGlhbS4=' &&
191192
$arg['attachments'][0]['content_id'] === 'lorem-ipsum.txt' &&
192193
$arg['attachments'][0]['content_type'] === 'text/plain';
193-
}))
194+
}), Mockery::type('array'))
194195
->andReturn($apiResponse);
195196

196197
$this->transporter->send($email);
@@ -228,7 +229,7 @@
228229
$arg['attachments'][0]['content_type'] === 'text/calendar' &&
229230
$arg['attachments'][0]['content'] === $calendarContent &&
230231
$arg['attachments'][0]['filename'] === 'invite.ics';
231-
}))
232+
}), Mockery::type('array'))
232233
->andReturn($apiResponse);
233234

234235
$this->transporter->send($email);
@@ -268,7 +269,7 @@
268269
return $arg['from'] === 'from@example.com' &&
269270
$arg['to'] === ['"Acme" <to@example.com>'] &&
270271
$arg['subject'] === 'Test Subject';
271-
}))
272+
}), Mockery::type('array'))
272273
->andReturn($apiResponse);
273274

274275
$message = $this->transporter->send($email);
@@ -286,3 +287,63 @@
286287
->getValue()
287288
)->toBe('49a3999c-0ce1-4ea6-ab68-afcd6dc2e794');
288289
});
290+
291+
it('can send with tags', function () {
292+
$email = (new SymfonyEmail())
293+
->from('from@example.com')
294+
->to(new Address('to@example.com', 'Acme'))
295+
->subject('Test Subject')
296+
->text('Test plain text body');
297+
$email->getHeaders()->add(new MetadataHeader('category', 'confirm_email'));
298+
$email->getHeaders()->add(new MetadataHeader('customer_id', '123'));
299+
300+
$apiResponse = new Email([
301+
'id' => '49a3999c-0ce1-4ea6-ab68-afcd6dc2e794',
302+
]);
303+
304+
$this->client->emails
305+
->shouldReceive('send')
306+
->once()
307+
->with(Mockery::on(function ($arg) {
308+
return $arg['from'] === 'from@example.com' &&
309+
$arg['to'] === ['"Acme" <to@example.com>'] &&
310+
$arg['subject'] === 'Test Subject' &&
311+
$arg['tags'] === [
312+
['name' => 'category', 'value' => 'confirm_email'],
313+
['name' => 'customer_id', 'value' => '123'],
314+
] &&
315+
! array_key_exists('X-Metadata-category', $arg['headers']) &&
316+
! array_key_exists('X-Metadata-customer_id', $arg['headers']);
317+
}), Mockery::type('array'))
318+
->andReturn($apiResponse);
319+
320+
$this->transporter->send($email);
321+
});
322+
323+
it('can send with idempotency key', function () {
324+
$email = (new SymfonyEmail())
325+
->from('from@example.com')
326+
->to(new Address('to@example.com', 'Acme'))
327+
->subject('Test Subject')
328+
->text('Test plain text body');
329+
$email->getHeaders()->addTextHeader('Resend-Idempotency-Key', 'welcome-user/123456789');
330+
331+
$apiResponse = new Email([
332+
'id' => '49a3999c-0ce1-4ea6-ab68-afcd6dc2e794',
333+
]);
334+
335+
$this->client->emails
336+
->shouldReceive('send')
337+
->once()
338+
->with(Mockery::on(function ($arg) {
339+
return $arg['from'] === 'from@example.com' &&
340+
$arg['to'] === ['"Acme" <to@example.com>'] &&
341+
$arg['subject'] === 'Test Subject' &&
342+
! array_key_exists('Resend-Idempotency-Key', $arg['headers']);
343+
}), Mockery::on(function ($options) {
344+
return $options === ['idempotency_key' => 'welcome-user/123456789'];
345+
}))
346+
->andReturn($apiResponse);
347+
348+
$this->transporter->send($email);
349+
});

0 commit comments

Comments
 (0)