Open
Description
This is sample code for SES driver, maybe you can test it
<?php
namespace Vormkracht10\Mails\Drivers;
use Illuminate\Http\Client\Response;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\URL;
use Vormkracht10\Mails\Contracts\MailDriverContract;
use Vormkracht10\Mails\Enums\EventType;
use Vormkracht10\Mails\Enums\Provider;
class SesDriver extends MailDriver implements MailDriverContract
{
public function registerWebhooks($components): void
{
$trackingConfig = (array) config('mails.logging.tracking');
$webhookUrl = URL::signedRoute('mails.webhook', ['provider' => Provider::SES]);
$region = config('services.ses.region', 'us-east-1');
$key = config('services.ses.key');
$secret = config('services.ses.secret');
// Create SNS topic if it doesn't exist
$snsClient = Http::withBasicAuth($key, $secret)
->asJson()
->baseUrl("https://sns.$region.amazonaws.com");
// Create or get topic
$topicResponse = $snsClient->post('/', [
'Action' => 'CreateTopic',
'Name' => 'mail-tracking-webhook',
'Version' => '2010-03-31',
]);
if ($topicResponse->successful()) {
$topicArn = $topicResponse->json()['CreateTopicResponse']['CreateTopicResult']['TopicArn'] ?? null;
if ($topicArn) {
$components->info("SES SNS topic created: $topicArn");
// Subscribe endpoint to topic
$subscribeResponse = $snsClient->post('/', [
'Action' => 'Subscribe',
'TopicArn' => $topicArn,
'Protocol' => 'https',
'Endpoint' => $webhookUrl,
'Version' => '2010-03-31',
]);
if ($subscribeResponse->successful()) {
$subscriptionArn = $subscribeResponse->json()['SubscribeResponse']['SubscribeResult']['SubscriptionArn'] ?? null;
$components->info("SES webhook subscribed: $subscriptionArn");
} else {
$components->error('Failed to subscribe SES webhook to SNS topic');
}
// Configure SES to publish to SNS
$sesClient = Http::withBasicAuth($key, $secret)
->asJson()
->baseUrl("https://email.$region.amazonaws.com");
$configurationSetName = 'mail-tracking-set';
// Create configuration set if it doesn't exist
$createConfigSetResponse = $sesClient->post('/', [
'Action' => 'CreateConfigurationSet',
'ConfigurationSet' => [
'Name' => $configurationSetName,
],
'Version' => '2010-12-01',
]);
if ($createConfigSetResponse->successful() || strpos($createConfigSetResponse->body(), 'ConfigurationSetAlreadyExists') !== false) {
$components->info("SES configuration set ready: $configurationSetName");
// Set up event destinations for each enabled event type
$eventTypes = [];
if ((bool) $trackingConfig['opens']) {
$eventTypes[] = 'Open';
}
if ((bool) $trackingConfig['clicks']) {
$eventTypes[] = 'Click';
}
if ((bool) $trackingConfig['deliveries']) {
$eventTypes[] = 'Delivery';
}
if ((bool) $trackingConfig['bounces']) {
$eventTypes[] = 'Bounce';
}
if ((bool) $trackingConfig['complaints']) {
$eventTypes[] = 'Complaint';
}
if ((bool) $trackingConfig['unsubscribes']) {
$eventTypes[] = 'Subscription';
}
if (count($eventTypes) > 0) {
$eventDestResponse = $sesClient->post('/', [
'Action' => 'CreateConfigurationSetEventDestination',
'ConfigurationSetName' => $configurationSetName,
'EventDestination' => [
'Name' => 'mail-tracking-destination',
'Enabled' => true,
'MatchingEventTypes' => $eventTypes,
'SNSDestination' => [
'TopicARN' => $topicArn,
],
],
'Version' => '2010-12-01',
]);
if ($eventDestResponse->successful() || strpos($eventDestResponse->body(), 'EventDestinationAlreadyExists') !== false) {
$components->info('SES event destination configured successfully');
} else {
$components->error('Failed to configure SES event destination');
}
}
} else {
$components->error('Failed to create SES configuration set');
}
} else {
$components->error('Failed to get SES SNS topic ARN');
}
} else {
$components->error('Failed to create SES SNS topic');
}
}
public function verifyWebhookSignature(array $payload): bool
{
if (app()->runningUnitTests()) {
return true;
}
// SES/SNS signature verification
if (isset($payload['Type']) && $payload['Type'] === 'SubscriptionConfirmation') {
// Auto-confirm subscription
if (isset($payload['SubscribeURL'])) {
Http::get($payload['SubscribeURL']);
return true;
}
return false;
}
if (! isset($payload['Signature']) || ! isset($payload['SigningCertURL'])) {
return false;
}
// Get the certificate
$certificate = Http::get($payload['SigningCertURL'])->body();
// Build the message
$message = '';
if (isset($payload['Type'])) {
$message .= "Type\n" . $payload['Type'] . "\n";
}
if (isset($payload['MessageId'])) {
$message .= "MessageId\n" . $payload['MessageId'] . "\n";
}
if (isset($payload['Timestamp'])) {
$message .= "Timestamp\n" . $payload['Timestamp'] . "\n";
}
if (isset($payload['TopicArn'])) {
$message .= "TopicArn\n" . $payload['TopicArn'] . "\n";
}
if (isset($payload['Message'])) {
$message .= "Message\n" . $payload['Message'] . "\n";
}
// Verify the signature
$cert = openssl_pkey_get_public($certificate);
$signature = base64_decode($payload['Signature']);
$result = openssl_verify($message, $signature, $cert, OPENSSL_ALGO_SHA1);
return $result === 1;
}
public function attachUuidToMail(MessageSending $event, string $uuid): MessageSending
{
// Set configuration set header
$event->message->getHeaders()->addTextHeader('X-SES-CONFIGURATION-SET', 'mail-tracking-set');
// Set custom UUID as message tag
$event->message->getHeaders()->addTextHeader('X-SES-MESSAGE-TAGS', config('mails.headers.uuid') . '=' . $uuid);
return $event;
}
public function getUuidFromPayload(array $payload): ?string
{
// Extract message from SNS notification
$message = isset($payload['Message']) ? json_decode($payload['Message'], true) : $payload;
if (! is_array($message)) {
return null;
}
// Get mail data from the SES message
$mail = $message['mail'] ?? null;
if (! $mail) {
return null;
}
// Look for the UUID in message tags
if (isset($mail['tags'][config('mails.headers.uuid')])) {
return $mail['tags'][config('mails.headers.uuid')][0] ?? null;
}
return null;
}
protected function getTimestampFromPayload(array $payload): string
{
// Extract message from SNS notification
$message = isset($payload['Message']) ? json_decode($payload['Message'], true) : $payload;
if (! is_array($message)) {
return now();
}
// Get timestamp based on the event type
if (isset($message['eventType'])) {
switch ($message['eventType']) {
case 'Delivery':
return $message['delivery']['timestamp'] ?? now();
case 'Bounce':
return $message['bounce']['timestamp'] ?? now();
case 'Complaint':
return $message['complaint']['timestamp'] ?? now();
case 'Open':
return $message['open']['timestamp'] ?? now();
case 'Click':
return $message['click']['timestamp'] ?? now();
default:
return $message['mail']['timestamp'] ?? now();
}
}
return $message['mail']['timestamp'] ?? now();
}
public function eventMapping(): array
{
return [
EventType::CLICKED->value => ['eventType' => 'Click'],
EventType::COMPLAINED->value => ['eventType' => 'Complaint'],
EventType::DELIVERED->value => ['eventType' => 'Delivery'],
EventType::HARD_BOUNCED->value => ['eventType' => 'Bounce', 'bounce.bounceType' => 'Permanent'],
EventType::OPENED->value => ['eventType' => 'Open'],
EventType::SOFT_BOUNCED->value => ['eventType' => 'Bounce', 'bounce.bounceType' => 'Transient'],
EventType::UNSUBSCRIBED->value => ['eventType' => 'Complaint', 'complaint.complaintFeedbackType' => 'not-spam'],
];
}
public function dataMapping(): array
{
return [
'browser' => 'open.userAgent',
'city' => 'mail.commonHeaders.from', // SES doesn't provide geolocation data
'country_code' => null,
'ip_address' => 'open.ipAddress',
'link' => 'click.link',
'os' => null, // SES doesn't provide detailed OS information
'platform' => null, // SES doesn't provide platform data
'tag' => 'mail.tags',
'user_agent' => 'open.userAgent',
];
}
public function unsuppressEmailAddress(string $address): Response
{
$region = config('services.ses.region', 'us-east-1');
$key = config('services.ses.key');
$secret = config('services.ses.secret');
$client = Http::withBasicAuth($key, $secret)
->asJson()
->baseUrl("https://email.$region.amazonaws.com");
return $client->post('/', [
'Action' => 'DeleteIdentity',
'Identity' => $address,
'Version' => '2010-12-01',
]);
}
}
Metadata
Metadata
Assignees
Labels
No labels