diff --git a/.env b/.env index fd054dfa..e7a66846 100644 --- a/.env +++ b/.env @@ -41,3 +41,4 @@ USPS_PASSWORD= CAMPAIGN_MONITOR_API_KEY= CAMPAIGN_MONITOR_DEFAULT_LIST_ID= +CAMPAIGN_MONITOR_WEBHOOK_AUTH_KEY= diff --git a/config/packages/campaign_monitor.yaml b/config/packages/campaign_monitor.yaml index e2f40cd3..c6775d77 100644 --- a/config/packages/campaign_monitor.yaml +++ b/config/packages/campaign_monitor.yaml @@ -1,3 +1,4 @@ parameters: campaign_monitor.api_key: '%env(CAMPAIGN_MONITOR_API_KEY)%' campaign_monitor.default_list_id: '%env(CAMPAIGN_MONITOR_DEFAULT_LIST_ID)%' + campaign_monitor.webhook_token: '%env(CAMPAIGN_MONITOR_WEBHOOK_TOKEN)%' diff --git a/src/Controller/UpdateController.php b/src/Controller/UpdateController.php index b334ba0f..c1b7cbce 100644 --- a/src/Controller/UpdateController.php +++ b/src/Controller/UpdateController.php @@ -2,12 +2,10 @@ namespace App\Controller; +use App\Service\EmailService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Mime\Header\Headers; -use Symfony\Component\Mailer\MailerInterface; use App\Entity\Member; use App\Form\MemberUpdateType; @@ -35,7 +33,7 @@ public function updateFromQueryString(Request $request) /** * @Route("/update-my-info/{externalIdentifier}/{updateToken}", name="self_service_update") */ - public function update(Request $request, MailerInterface $mailer) + public function update(Request $request, EmailService $emailService) { $member = $this->getDoctrine()->getRepository(Member::class)->findOneBy([ 'externalIdentifier' => $request->get('externalIdentifier') @@ -55,25 +53,10 @@ public function update(Request $request, MailerInterface $mailer) $member = $form->getData(); // If form is submitted, member is no longer "lost" $member->setIsLost(false); - // Set headers for grouping in transactional email reporting - $headers = new Headers(); - $headers->addTextHeader('X-Cmail-GroupName', 'Member Record Update'); - $headers->addTextHeader('X-MC-Tags', 'Member Record Update'); - $message = new TemplatedEmail($headers); - $message - ->to($this->getParameter('app.email.to')) - ->from($this->getParameter('app.email.from')) - ->subject(sprintf('Member Record Update: %s', $member->getDisplayName())) - ->htmlTemplate('update/email_update.html.twig') - ->context(['member' => $member]) - ; - if ($member->getPrimaryEmail()) { - $message->replyTo($member->getPrimaryEmail()); - } $entityManager = $this->getDoctrine()->getManager(); $entityManager->persist($member); $entityManager->flush(); - $mailer->send($message); + $emailService->sendMemberUpdate($member); return $this->render('update/confirmation.html.twig'); } diff --git a/src/Controller/WebhookController.php b/src/Controller/WebhookController.php new file mode 100644 index 00000000..03775490 --- /dev/null +++ b/src/Controller/WebhookController.php @@ -0,0 +1,69 @@ +json([ + 'status' => 200, + 'title' => 'Success', + 'message' => 'Webhooks are available.' + ]); + } + + /** + * @Route("/webhook/email-service", name="webhook_email_service", methods={"POST"}) + */ + public function emailServiceWebhook(Request $request, EmailService $emailService): Response + { + // Fail if not configured + if (!$emailService->isConfigured()) { + return $this->json([ + 'status' => 500, + 'title' => 'Internal server error', + 'details' => 'Email service not configured.' + ], 500); + } + + // Fail if token is missing or mismatched + if (!$request->get('token') || + $request->get('token') != $emailService->getWebhookToken() + ) { + return $this->json([ + 'status' => 403, + 'title' => 'Access denied', + 'details' => 'Invalid credentials.' + ], 403); + } + + // Process payload + try { + $output = $emailService->processWebhookBody($request->getContent()); + return $this->json([ + 'status' => 200, + 'title' => 'success', + 'details' => 'Processed webhook.', + 'extra' => $output + ]); + } catch (\Exception $e) { + return $this->json([ + 'status' => 'error', + 'title' => 'Internal server error', + 'message' => $e->getMessage() + ], 500); + } + } + + +} diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php index 0f3c4e21..e1047e39 100644 --- a/src/Service/EmailService.php +++ b/src/Service/EmailService.php @@ -2,9 +2,13 @@ namespace App\Service; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use CS_REST_Subscribers; use CS_REST_Campaigns; +use CS_REST_Subscribers; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Header\Headers; use App\Entity\Member; @@ -16,15 +20,22 @@ class EmailService protected $defaultListId; + protected $webhookToken; + protected $client; protected $campaignsClient; - public function __construct(ParameterBagInterface $params) + protected $em; + + protected $mailer; + + public function __construct(ParameterBagInterface $params, EntityManagerInterface $em, MailerInterface $mailer) { $this->params = $params; $this->apiKey = $params->get('campaign_monitor.api_key'); $this->defaultListId = $params->get('campaign_monitor.default_list_id'); + $this->webhookToken = $params->get('campaign_monitor.webhook_token'); $this->client = new CS_REST_Subscribers( $this->defaultListId, [ @@ -37,6 +48,8 @@ public function __construct(ParameterBagInterface $params) 'api_key' => $this->apiKey ] ); + $this->em = $em; + $this->mailer = $mailer; } public function isConfigured(): bool @@ -138,6 +151,78 @@ public function getCampaignById($campaignId): object return $result->response; } + public function getWebhookToken(): string + { + return $this->webhookToken; + } + + public function processWebhookBody(string $content): array + { + $content = json_decode($content, null, $depth=512, JSON_THROW_ON_ERROR); + if (!property_exists($content, 'Events') || !is_array($content->Events)) { + throw new \Exception('Invalid webhook payload. Must have Events.'); + } + $memberRepository = $this->em->getRepository(Member::class); + $output = []; + foreach ($content->Events as $event) { + switch($event->Type) { + case 'Update': + $member = $memberRepository->findOneBy([ + 'primaryEmail' => $event->OldEmailAddress + ]); + if (!$member) { + $output[] = [ + 'result' => sprintf( + 'Unable to locate member with %s', + $event->OldEmailAddress + ), + 'payload' => $event + ]; + break; + } + $member->setPrimaryEmail($event->EmailAddress); + $this->sendMemberUpdate($member); + $this->em->persist($member); + $this->em->flush(); + $output[] = [ + 'result' => sprintf( + 'Email for %s updated from %s to %s', + $member, + $event->OldEmailAddress, + $event->EmailAddress + ), + 'payload' => $event + ]; + break; + default: + $output[] = [ + 'result' => 'No action taken.', + 'payload' => $event + ]; + } + } + return $output; + } + + public function sendMemberUpdate(Member $member): void + { + $headers = new Headers(); + $headers->addTextHeader('X-Cmail-GroupName', 'Member Record Update'); + $headers->addTextHeader('X-MC-Tags', 'Member Record Update'); + $message = new TemplatedEmail($headers); + $message + ->to($this->params->get('app.email.to')) + ->from($this->params->get('app.email.from')) + ->subject(sprintf('Member Record Update: %s', $member->getDisplayName())) + ->htmlTemplate('update/email_update.html.twig') + ->context(['member' => $member]) + ; + if ($member->getPrimaryEmail()) { + $message->replyTo($member->getPrimaryEmail()); + } + $this->mailer->send($message); + } + /* Private Methods */ private function buildCustomFieldArray(Member $member): array