HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/hi.naniguide.com/app/Http/Controllers/Webhook/SendGridWebhookController.php
<?php

namespace App\Http\Controllers\Webhook;

use App\Http\Controllers\Controller;
use App\Models\CampaignLog;
use App\Models\CampaignRecipient;
use App\Models\ListSubscriber;
use App\Models\SuppressionList;
use App\Services\ComplaintService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class SendGridWebhookController extends Controller
{
    public function __construct(
        protected ComplaintService $complaintService
    ) {}

    /**
     * Handle SendGrid webhook events.
     */
    public function handle(Request $request)
    {
        // SendGrid sends an array of events
        $events = $request->input();

        if (!is_array($events) || empty($events)) {
            return response()->json(['error' => 'Invalid payload'], 400);
        }

        $processed = 0;

        foreach ($events as $event) {
            if (!is_array($event)) {
                continue;
            }

            $eventType = $event['event'] ?? null;

            // Handle spam report (complaint)
            if ($eventType === 'spamreport') {
                $this->handleComplaint($event);
                $processed++;
            }

            // Handle bounce (optional)
            if ($eventType === 'bounce' || $eventType === 'dropped') {
                $this->handleBounce($request, $event);
                $processed++;
            }

            if ($eventType === 'open') {
                $this->handleOpen($request, $event);
                $processed++;
            }

            if ($eventType === 'click') {
                $this->handleClick($request, $event);
                $processed++;
            }
        }

        return response()->json(['status' => 'processed', 'count' => $processed], 200);
    }

    /**
     * Handle spam complaint event.
     */
    protected function handleComplaint(array $event): void
    {
        $email = $event['email'] ?? null;
        $messageId = $event['sg_message_id'] ?? null;

        if (!$email) {
            Log::warning('SendGrid complaint event missing email', ['event' => $event]);
            return;
        }

        try {
            $this->complaintService->processComplaint(
                email: $email,
                provider: 'sendgrid',
                providerMessageId: $messageId,
                feedbackId: $event['sg_event_id'] ?? null,
                rawData: json_encode($event),
                meta: $event
            );
        } catch (\Exception $e) {
            Log::error('Error processing SendGrid complaint: ' . $e->getMessage(), [
                'event' => $event,
            ]);
        }
    }

    /**
     * Handle bounce event (optional).
     */
    protected function handleBounce(Request $request, array $event): void
    {
        $eventType = $event['event'] ?? null;
        $recipient = $this->resolveRecipientFromEvent($event);
        if (!$recipient) {
            return;
        }

        $campaign = $recipient->campaign;
        $email = (string) ($recipient->email ?? ($event['email'] ?? ''));

        $reason = (string) (
            $event['reason']
            ?? $event['response']
            ?? $event['smtp-id']
            ?? 'SendGrid reported delivery failure'
        );

        $shouldMarkAsBounce = $eventType === 'bounce';
        $isHardBounce = $shouldMarkAsBounce;

        if ($shouldMarkAsBounce) {
            $alreadyBounced = $recipient->status === 'bounced';
            $deliveryServerMeta = [];
            if (!$alreadyBounced) {
                $recipient->markAsBounced();
                if ($campaign) {
                    $deliveryServerMeta = $this->resolveDeliveryServerMeta($recipient, $campaign?->deliveryServer);

                    $campaign->incrementBouncedCount();
                }
                CampaignLog::logEvent(
                    $campaign?->id ?? 0,
                    'bounced',
                    $recipient->id,
                    [
                        'email' => $email,
                        'provider' => 'sendgrid',
                        'reason' => $reason,
                    ] + ($deliveryServerMeta ?? []),
                    $request->ip(),
                    $request->userAgent(),
                    null,
                    $reason
                );
            }
        } else {
            $alreadyFailed = $recipient->status === 'failed';
            $deliveryServerMeta = [];
            if (!$alreadyFailed) {
                $recipient->markAsFailed($reason);
                if ($campaign) {
                    $deliveryServerMeta = $this->resolveDeliveryServerMeta($recipient, $campaign?->deliveryServer);

                    $campaign->incrementFailedCount();
                }
                CampaignLog::logEvent(
                    $campaign?->id ?? 0,
                    'failed',
                    $recipient->id,
                    [
                        'email' => $email,
                        'provider' => 'sendgrid',
                        'reason' => $reason,
                    ] + ($deliveryServerMeta ?? []),
                    $request->ip(),
                    $request->userAgent(),
                    null,
                    $reason
                );
            }
        }

        $subscriber = null;
        if (is_string($email) && trim($email) !== '') {
            $subscriber = ListSubscriber::query()
                ->when($campaign?->list_id, fn ($q) => $q->where('list_id', $campaign->list_id))
                ->where('email', strtolower(trim($email)))
                ->first();
        }

        if ($subscriber && $isHardBounce) {
            $subscriber->update([
                'status' => 'bounced',
                'is_bounced' => true,
                'bounced_at' => now(),
                'suppressed_at' => now(),
            ]);

            SuppressionList::firstOrCreate(
                [
                    'customer_id' => $subscriber->list?->customer_id,
                    'email' => $subscriber->email,
                ],
                [
                    'reason' => 'bounce',
                    'reason_description' => $reason,
                    'subscriber_id' => $subscriber->id,
                    'campaign_id' => $campaign?->id,
                    'suppressed_at' => now(),
                ]
            );
        }

        Log::info('SendGrid bounce processed', [
            'event' => $eventType,
            'email' => $email,
            'reason' => $reason,
            'campaign_id' => $campaign?->id,
            'recipient_id' => $recipient->id,
        ]);
    }

    protected function handleOpen(Request $request, array $event): void
    {
        $recipient = $this->resolveRecipientFromEvent($event);
        if (!$recipient) {
            return;
        }

        $campaign = $recipient->campaign;
        if ($campaign && $campaign->track_opens && !$recipient->isOpened()) {
            $recipient->markAsOpened();
            $campaign->incrementOpenedCount();

            CampaignLog::logEvent(
                $campaign->id,
                'opened',
                $recipient->id,
                ['email' => $recipient->email, 'provider' => 'sendgrid'],
                $request->ip(),
                $request->userAgent()
            );
        }
    }

    protected function handleClick(Request $request, array $event): void
    {
        $recipient = $this->resolveRecipientFromEvent($event);
        if (!$recipient) {
            return;
        }

        $campaign = $recipient->campaign;
        $url = $event['url'] ?? null;

        if ($campaign && $campaign->track_clicks) {
            $wasOpened = $recipient->isOpened();
            $wasClicked = $recipient->isClicked();

            if ($campaign->track_opens && !$wasOpened) {
                $recipient->markAsOpened();
                $campaign->incrementOpenedCount();
            }

            if (!$wasClicked) {
                $recipient->markAsClicked();
                $campaign->incrementClickedCount();
            }

            CampaignLog::logEvent(
                $campaign->id,
                'clicked',
                $recipient->id,
                ['email' => $recipient->email, 'provider' => 'sendgrid'],
                $request->ip(),
                $request->userAgent(),
                is_string($url) ? $url : null
            );
        }
    }

    protected function resolveDeliveryServerMeta(CampaignRecipient $recipient, ?\App\Models\DeliveryServer $fallbackServer = null): array
    {
        $meta = is_array($recipient->meta) ? $recipient->meta : [];

        $deliveryServerId = (int) ($meta['delivery_server_id'] ?? 0);
        $deliveryServerName = trim((string) ($meta['delivery_server_name'] ?? ''));
        $deliveryServerFromEmail = trim((string) ($meta['delivery_server_from_email'] ?? ''));

        if ($deliveryServerId <= 0 && $fallbackServer) {
            $deliveryServerId = (int) $fallbackServer->id;
            if ($deliveryServerName === '') {
                $deliveryServerName = (string) $fallbackServer->name;
            }
            if ($deliveryServerFromEmail === '') {
                $deliveryServerFromEmail = (string) $fallbackServer->from_email;
            }
        }

        return array_filter([
            'delivery_server_id' => $deliveryServerId > 0 ? $deliveryServerId : null,
            'delivery_server_name' => $deliveryServerName !== '' ? $deliveryServerName : null,
            'delivery_server_from_email' => $deliveryServerFromEmail !== '' ? $deliveryServerFromEmail : null,
        ], static fn ($value) => $value !== null);
    }

    protected function resolveRecipientFromEvent(array $event): ?CampaignRecipient
    {
        $uuid = $event['X-Recipient-UUID'] ?? $event['x-recipient-uuid'] ?? $event['recipient_uuid'] ?? null;
        if (is_string($uuid) && trim($uuid) !== '') {
            $found = CampaignRecipient::where('uuid', trim($uuid))->first();
            if ($found) {
                return $found;
            }
        }

        $email = $event['email'] ?? null;
        if (!is_string($email) || trim($email) === '') {
            return null;
        }

        return CampaignRecipient::where('email', trim($email))
            ->latest('id')
            ->first();
    }
}