From 0437ff8aeca0c9797991479ee97458ed68cb9999 Mon Sep 17 00:00:00 2001 From: Neophytis <37024002+Neophytis@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:42:40 +0200 Subject: [PATCH] feat(newsletter): add List-Unsubscribe, Precedence, and one-click POST handler Adds bulk-mail headers required by Google/Yahoo since Feb 2024 to all regular newsletter sends: List-Unsubscribe (personalised per recipient using the existing unsubscribe token), List-Unsubscribe-Post for Gmail's one-click unsubscribe button, and Precedence: bulk. Transactional sends (reminder, suspension, TOU) are excluded. Also handles RFC 8058 one-click POST requests in SubscriptionController so Gmail's unsubscribe button works without a confirmation page. --- src/Controller/SubscriptionController.php | 6 ++++ src/Service/Mailer.php | 40 +++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Controller/SubscriptionController.php b/src/Controller/SubscriptionController.php index 0f0cb61699..a5981b7be2 100644 --- a/src/Controller/SubscriptionController.php +++ b/src/Controller/SubscriptionController.php @@ -22,6 +22,12 @@ public function unsubscribeNewsletter( string $username, string $unsubscribeKey ): Response { + if ($request->isMethod('POST') && 'List-Unsubscribe=One-Click' === $request->request->get('List-Unsubscribe')) { + $subscriptionModel->unsubscribeNewsletter($username, $unsubscribeKey); + + return new Response('', Response::HTTP_OK); + } + $form = $this->createForm(NewsletterUnsubscribeType::class); $form->handleRequest($request); diff --git a/src/Service/Mailer.php b/src/Service/Mailer.php index 7c6818b5bd..0675cbe725 100644 --- a/src/Service/Mailer.php +++ b/src/Service/Mailer.php @@ -133,7 +133,9 @@ public function sendNewsletterEmail(Newsletter $newsletter, Member $receiver, ar $parameters['sender'], $receiver, 'newsletter', - $parameters + $parameters, + null, + $this->buildNewsletterHeaders($newsletter, $receiver, $parameters), ); } @@ -301,7 +303,7 @@ private function getBeWelcomeAddress(Member $sender, $email): Address * * @return bool */ - private function sendTemplateEmail($sender, $receiver, string $template, array $parameters, ?string $replyTo = null): bool + private function sendTemplateEmail($sender, $receiver, string $template, array $parameters, ?string $replyTo = null, array $extraTextHeaders = []): bool { $currentLocale = $this->translator->getLocale(); $success = true; @@ -344,6 +346,10 @@ private function sendTemplateEmail($sender, $receiver, string $template, array $ $email->replyTo(new Address($replyTo)); } + foreach ($extraTextHeaders as $name => $value) { + $email->getHeaders()->addTextHeader($name, $value); + } + try { $this->mailer->send($email); } catch (TransportExceptionInterface $e) { @@ -363,6 +369,36 @@ private function setTranslatorLocale(Member $receiver) $this->translator->setLocale($language->getShortCode()); } + private function buildNewsletterHeaders(Newsletter $newsletter, Member $receiver, array $parameters): array + { + $transactionalTypes = [ + 'RemindToLog', + 'MailToConfirmReminder', + Newsletter::SUSPENSION_NOTIFICATION, + Newsletter::TERMS_OF_USE, + ]; + if (\in_array($newsletter->getType(), $transactionalTypes, true)) { + return []; + } + + $unsubscribeKey = $parameters['unsubscribe_key'] ?? null; + if (null === $unsubscribeKey) { + return []; + } + + $unsubscribeUrl = $this->urlGenerator->generate( + 'regular_newsletter_unsubscribe', + ['username' => $receiver->getUsername(), 'unsubscribeKey' => $unsubscribeKey], + UrlGeneratorInterface::ABSOLUTE_URL + ); + + return [ + 'Precedence' => 'bulk', + 'List-Unsubscribe' => '<' . $unsubscribeUrl . '>', + 'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click', + ]; + } + private function prepareParametersForNewsletter(Newsletter $newsletter, Member $receiver): array { $newsletterType = $newsletter->getType();