Skip to content

Support for SES? #26

Open
Open
@faizananwerali

Description

@faizananwerali

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions