182 lines
5.2 KiB
PHP
182 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\EmailBounce;
|
|
use App\Models\Mailing;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class MailgunWebhookController extends Controller
|
|
{
|
|
/**
|
|
* Handle Mailgun webhook events
|
|
*/
|
|
public function handle(Request $request)
|
|
{
|
|
// Verify webhook signature (recommended for production)
|
|
if (!$this->verifySignature($request)) {
|
|
Log::warning('Invalid Mailgun webhook signature');
|
|
return response('Unauthorized', 401);
|
|
}
|
|
|
|
$eventData = $request->input('event-data', []);
|
|
$event = $eventData['event'] ?? null;
|
|
|
|
Log::info('Mailgun webhook received', ['event' => $event, 'data' => $eventData]);
|
|
|
|
switch ($event) {
|
|
case 'bounced':
|
|
$this->handleBounce($eventData);
|
|
break;
|
|
case 'complained':
|
|
$this->handleComplaint($eventData);
|
|
break;
|
|
case 'unsubscribed':
|
|
$this->handleUnsubscribe($eventData);
|
|
break;
|
|
default:
|
|
Log::info('Unhandled Mailgun event', ['event' => $event]);
|
|
}
|
|
|
|
return response('OK', 200);
|
|
}
|
|
|
|
/**
|
|
* Handle bounce events
|
|
*/
|
|
protected function handleBounce(array $eventData)
|
|
{
|
|
$email = $eventData['recipient'] ?? null;
|
|
$errorCode = $eventData['delivery-status']['code'] ?? null;
|
|
$errorMessage = $eventData['delivery-status']['description'] ?? 'Unknown bounce';
|
|
|
|
if (!$email) {
|
|
Log::warning('Bounce event missing recipient email', $eventData);
|
|
return;
|
|
}
|
|
|
|
// Determine bounce type based on error code
|
|
$bounceType = $this->determineBounceTypeFromCode($errorCode);
|
|
|
|
// Extract mailing ID from message tags or headers if available
|
|
$mailingId = $this->extractMailingId($eventData);
|
|
|
|
EmailBounce::recordBounce($email, $bounceType, $errorMessage, $mailingId);
|
|
|
|
Log::info("Recorded {$bounceType} bounce", [
|
|
'email' => $email,
|
|
'code' => $errorCode,
|
|
'message' => $errorMessage,
|
|
'mailing_id' => $mailingId,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle complaint (spam) events
|
|
*/
|
|
protected function handleComplaint(array $eventData)
|
|
{
|
|
$email = $eventData['recipient'] ?? null;
|
|
|
|
if (!$email) {
|
|
Log::warning('Complaint event missing recipient email', $eventData);
|
|
return;
|
|
}
|
|
|
|
$mailingId = $this->extractMailingId($eventData);
|
|
|
|
EmailBounce::recordBounce($email, 'complaint', 'Spam complaint', $mailingId);
|
|
|
|
Log::warning("Recorded spam complaint", [
|
|
'email' => $email,
|
|
'mailing_id' => $mailingId,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle unsubscribe events
|
|
*/
|
|
protected function handleUnsubscribe(array $eventData)
|
|
{
|
|
$email = $eventData['recipient'] ?? null;
|
|
|
|
if (!$email) {
|
|
Log::warning('Unsubscribe event missing recipient email', $eventData);
|
|
return;
|
|
}
|
|
|
|
// You might want to handle unsubscribes differently
|
|
// For now, we'll just log it
|
|
Log::info("Unsubscribe event", ['email' => $email]);
|
|
|
|
// Optionally suppress the email
|
|
// EmailBounce::suppressEmail($email, 'Unsubscribed via Mailgun');
|
|
}
|
|
|
|
/**
|
|
* Determine bounce type from Mailgun error code
|
|
*/
|
|
protected function determineBounceTypeFromCode($code): string
|
|
{
|
|
if (!$code) return 'unknown';
|
|
|
|
// Mailgun error codes for hard bounces
|
|
$hardBounceCodes = [550, 551, 553, 554];
|
|
|
|
// Mailgun error codes for soft bounces
|
|
$softBounceCodes = [421, 450, 451, 452, 552];
|
|
|
|
if (in_array($code, $hardBounceCodes)) {
|
|
return 'hard';
|
|
}
|
|
|
|
if (in_array($code, $softBounceCodes)) {
|
|
return 'soft';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Extract mailing ID from event data
|
|
*/
|
|
protected function extractMailingId(array $eventData): ?int
|
|
{
|
|
// Check if mailing ID is in message tags
|
|
$tags = $eventData['message']['headers']['message-id'] ?? '';
|
|
if (preg_match('/mailing-(\d+)/', $tags, $matches)) {
|
|
return (int) $matches[1];
|
|
}
|
|
|
|
// Check custom headers
|
|
$headers = $eventData['message']['headers'] ?? [];
|
|
if (isset($headers['X-Mailing-ID'])) {
|
|
return (int) $headers['X-Mailing-ID'];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Verify Mailgun webhook signature
|
|
*/
|
|
protected function verifySignature(Request $request): bool
|
|
{
|
|
$timestamp = $request->input('timestamp');
|
|
$token = $request->input('token');
|
|
$signature = $request->input('signature');
|
|
|
|
$signingKey = config('services.mailgun.webhook_key');
|
|
|
|
if (!$signingKey) {
|
|
Log::warning('Mailgun webhook signing key not configured');
|
|
return config('app.env') !== 'production'; // Skip verification in non-production
|
|
}
|
|
|
|
$expectedSignature = hash_hmac('sha256', $timestamp . $token, $signingKey);
|
|
|
|
return hash_equals($expectedSignature, $signature);
|
|
}
|
|
} |