From b04ca114ae224666fa4c1e0cdb71a41cbe2f7b5e Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Thu, 4 Jun 2026 15:34:29 +0200 Subject: [PATCH] feat: :sparkles: handle Mailjet spam complaints via webhook --- .../Controllers/Api/WebhookController.php | 32 +++++++++++ app/Jobs/SyncEmailOptOuts.php | 28 ++-------- app/Lib/Helpers/MailjetHelper.php | 55 ++++++++++++++++++- routes/api.php | 3 + 4 files changed, 93 insertions(+), 25 deletions(-) create mode 100644 app/Http/Controllers/Api/WebhookController.php diff --git a/app/Http/Controllers/Api/WebhookController.php b/app/Http/Controllers/Api/WebhookController.php new file mode 100644 index 0000000..f5d5682 --- /dev/null +++ b/app/Http/Controllers/Api/WebhookController.php @@ -0,0 +1,32 @@ +all(); + + if ($data["event"] === "spam") { + $campaign = MailjetHelper::getCampaign($data["mj_campaign_id"]); + if (!$campaign) { return; } + + $brand = Brand::query()->where('newsletter_email', $campaign['FromEmail'])->first(); + if (!$brand) { return; } + + MailjetHelper::unsubscribeContact($brand, $data["email"]); + } + } +} diff --git a/app/Jobs/SyncEmailOptOuts.php b/app/Jobs/SyncEmailOptOuts.php index d0d7521..f25bdce 100644 --- a/app/Jobs/SyncEmailOptOuts.php +++ b/app/Jobs/SyncEmailOptOuts.php @@ -4,8 +4,6 @@ namespace App\Jobs; use App\Lib\Helpers\MailjetHelper; use App\Models\BrandContactList; -use App\Models\CustomerInfo; -use App\Models\SingleOptin; use Carbon\Carbon; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; @@ -22,8 +20,8 @@ class SyncEmailOptOuts implements ShouldQueue $contacts = MailjetHelper::getContactLists(); foreach ($contacts as $contact) { - $listId = $contact['ID']; - $brandModel = BrandContactList::query()->where('mailjet_list_id', $listId)->with('brand')->first(); + $listId = $contact['ID']; + $brandModel = BrandContactList::query()->where('mailjet_list_id', $listId)->with('brand')->first(); if ($brandModel) { $unsubscribedContacts = MailjetHelper::getUnsubscribedContacts($listId, Carbon::now()->subDay()); @@ -33,27 +31,9 @@ class SyncEmailOptOuts implements ShouldQueue } foreach ($unsubscribedContacts as $unsubscribedContact) { - $contact = MailjetHelper::getContact($unsubscribedContact['ContactID']); - $crmCustomer = CustomerInfo::getFromEmail($brandModel->brand, $contact[0]["Email"])->first(); + $contact = MailjetHelper::getContact($unsubscribedContact['ContactID']); - if ($crmCustomer) { - $crmCustomer->opt_in = false; - - if (!$crmCustomer->saveOrFail()) { - throw new \Exception(sprintf('Optin status not changed for customer (%s) and brand (%d).', $crmCustomer->hashcode, $brandModel->brand->name)); - } - } else { - $singleOptin = SingleOptin::query()->where('email', $contact[0]['Email']) - ->whereHas('brand', function ($q) use ($brandModel) { - $q->whereId($brandModel->brand->getKey()); - })->first(); - - if ($singleOptin) { - if (!$singleOptin->delete()) { - throw new \Exception(sprintf('SingleOptin (%s) not deleted for brand (%d).', $singleOptin->hashcode, $brandModel->brand->name)); - } - } - } + MailjetHelper::unsubscribeContact($brandModel->brand, $contact[0]["Email"]); } } } diff --git a/app/Lib/Helpers/MailjetHelper.php b/app/Lib/Helpers/MailjetHelper.php index b1a94ba..78c133a 100644 --- a/app/Lib/Helpers/MailjetHelper.php +++ b/app/Lib/Helpers/MailjetHelper.php @@ -2,10 +2,32 @@ namespace App\Lib\Helpers; +use App\Models\Brand; +use App\Models\CustomerInfo; +use App\Models\SingleOptin; use Mailjet\Resources; class MailjetHelper { + /** + * Undocumented function + * + * @param integer $campaignId + * @return array + */ + public static function getCampaign(int $campaignId): array + { + $response = app('mailjetClient')->get(Resources::$Campaign, [ + 'id' => $campaignId + ]); + + if ($response->success()) { + return $response->getData(); + } else { + return []; + } + } + /** * Retrieves contact lists * @@ -106,7 +128,7 @@ class MailjetHelper /** * Uploads a CSV file - * + * * @param integer $mailjetListId * @param string $csvFile * @return array @@ -148,4 +170,35 @@ class MailjetHelper return []; } } + + /** + * Undocumented function + * + * @param \App\Models\Brand $brandModel + * @param string $email + * @return void + */ + public static function unsubscribeContact(Brand $brandModel, string $email): void + { + $crmCustomer = CustomerInfo::getFromEmail($brandModel, $email)->first(); + + if ($crmCustomer) { + $crmCustomer->opt_in = false; + + if (!$crmCustomer->saveOrFail()) { + throw new \Exception(sprintf('Optin status not changed for customer (%s) and brand (%d).', $crmCustomer->hashcode, $brandModel->name)); + } + } else { + $singleOptin = SingleOptin::query()->where('email', $email) + ->whereHas('brand', function ($q) use ($brandModel) { + $q->whereId($brandModel->getKey()); + })->first(); + + if ($singleOptin) { + if (!$singleOptin->delete()) { + throw new \Exception(sprintf('SingleOptin (%s) not deleted for brand (%d).', $singleOptin->hashcode, $brandModel->name)); + } + } + } + } } diff --git a/routes/api.php b/routes/api.php index a38372b..ecb7e7a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -15,6 +15,7 @@ use App\Http\Controllers\Api\V4\CustomerController as V4CustomerController; use App\Http\Controllers\Api\V4\OptinController as V4OptinController; use App\Http\Controllers\Api\V4\QuestionController as V4QuestionController; use App\Http\Controllers\Api\V4\StatsController as V4StatsController; +use App\Http\Controllers\Api\WebhookController; use Illuminate\Support\Facades\Route; /* @@ -140,3 +141,5 @@ Route::prefix('v4')->name('apiV4.')->group(function () { Route::post('/claim', [V4ClaimController::class, 'store'])->name('claim.store'); }); }); + +Route::post('/webhook/mailjet', [WebhookController::class, 'handleMailjetWebhook'])->name('webhook.mailjet');