Files
timebank-cc-public/app/Http/Controllers/MailgunWebhookController.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

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);
}
}