Aerendir / bundle-aws-ses-monitor

Symfony Bundle to manage AWS SES notifications through AWS SNS.

Home Page:http://aerendir.me

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Dispatch events on relevant events

Aerendir opened this issue · comments

Events that will be dispatched:

  • Recipient filtered out (before sending);
  • Recipient filtered out (after sending);
  • Bounce notification from AWS SES;
  • Complaint notification from AWS SES;
  • Delivery notification from AWS SES;

Requested in #31

I was missing this feature too and as a workaround, and copy/pasted the code into a controller since it was really hard, if even possible to actually inherit from Processors.

Since I create AWS identity and topic by other means I only need to listen for confirmation requests and notifications.

<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Doctrine\ORM\EntityManagerInterface;
use Aws\Sns\SnsClient;
use SerendipityHQ\Bundle\AwsSesMonitorBundle\Helper\MessageHelper;
use SerendipityHQ\Bundle\AwsSesMonitorBundle\SnsTypes;
use SerendipityHQ\Bundle\AwsSesMonitorBundle\Entity\Topic;


class SnsController extends Controller
{
    private $snsClient;
    private $messageHelper;
    private $entityManager;

    /**
     * @param EntityManagerInterface $entityManager
     * @param MessageHelper $messageHelper
     */
    public function __construct(SnsClient $snsClient, EntityManagerInterface $entityManager, MessageHelper $messageHelper) {
        $this->snsClient = $snsClient;
        $this->messageHelper = $messageHelper;
        $this->entityManager = $entityManager;
    }

    /**
     * Receive SNS events.
     *
     * @Route(name="webhook_sns", path="/hook/amazon-sns")
     * @Method({"POST"})
     *
     * @param Request $request
     * @return Response
     */
    public function processRequest(Request $request): Response
    {
        $messageTypeHeader = $request->headers->get('x-amz-sns-message-type');
        if (null === $messageTypeHeader) {
            throw new BadRequestHttpException('This request is invalid');
        }

        switch ($messageTypeHeader) {
            case SnsTypes::HEADER_TYPE_NOTIFICATION:
                return $this->processNotification($request);
            case SnsTypes::HEADER_TYPE_CONFIRM_SUBSCRIPTION:
                return $this->processSubscription($request);
            default:
                throw new \RuntimeException('We received a request with header "%s" but are not able to handle it. Please, add an handler to manage it.');
        }
    }

    public function processSubscription(Request $request): Response
    {
        $message = $this->messageHelper->buildMessageFromRequest($request);

        if (false === $this->messageHelper->validateNotification($message)) {
            return new Response('The message is invalid.', 403);
        }

        /** @var Topic|null $topic */
        $topic = $this->entityManager->getRepository(Topic::class)->findOneBy(['arn' => $message->offsetGet('TopicArn')]);

        if (null === $topic) {
            return new Response('Topic not found', 404);
        }

        $this->snsClient->confirmSubscription(
            [
                'TopicArn' => $topic->getArn(),
                'Token'    => $message->offsetGet('Token'),
            ]
        );

        $this->entityManager->flush();

        return new Response('OK', 200);
    }

    public function processNotification(Request $request): Response
    {
        $message = $this->messageHelper->buildMessageFromRequest($request);
        if (false === $this->messageHelper->validateNotification($message)) {
            return new Response('The message is invalid.', 403);
        }

        $notificationData = $this->messageHelper->extractMessageData($message);
        if (false === isset($notificationData['notificationType'])) {
            return new Response('Missed NotificationType.', 403);
        }

        if (SnsTypes::MESSAGE_TYPE_SUBSCRIPTION_SUCCESS === $notificationData['notificationType']) {
            return new Response('OK', 200);
        }

        $messageId = $notificationType['xxxxxx'];
        if (!$messageId) {
            return new Response('Missed Notification Message-ID.', 403);
        }

        switch ($notificationData['notificationType']) {
            case SnsTypes::MESSAGE_TYPE_BOUNCE:
                return $this->processBounce($notificationData);
            case SnsTypes::MESSAGE_TYPE_COMPLAINT:
                return $this->processComplaint($notificationData);
            case SnsTypes::MESSAGE_TYPE_DELIVERY:
                return $this->processDelivery($notificationData);
            default:
                return new Response('Notification type not understood', 403);
        }
    }

    public function processBounce(array $notification): Response
    {
        return new Response('', 200); // override
    }

    public function processComplaint(array $notification): Response
    {
        return new Response('', 200);
    }

    public function processDelivery(array $notification): Response
    {
        return new Response('', 200);
    }
}
  • MessageHelper and SnsTypes are a couple of define/wrapping lines
  • and I found that the need of database almost exclusively serves identity/topic creation and I'd personally prefer to store them inside config/, by allowing an arn yaml key under each identity.

That would allow people that only need confirmation+listen events to have an almost self-contained base class under 150 LoC