Webhook Postmark support: management of bounces, spam and unsubscription

I finished coding Webhook Postmark support.

Event webhooks (Bounce, Spam plain, SubscriptionChange) from Postmark to Mautic are supported with this change.

I’m using this code in production with Mautic 4.4.2 and it works like a charm.

I share here the portions of code to modify, I hope it will be useful to someone:

in file: /mautic/app/bundles/EmailBundle/Config/config.php

Replace:

'mautic.transport.postmark' => [
                'class' => 'Mautic\EmailBundle\Swiftmailer\Transport\PostmarkTransport',
                'serviceAlias' => 'swiftmailer.mailer.transport.%s',
                'methodCalls' => [
                    'setUsername' => ['%mautic.mailer_user%'],
                    'setPassword' => ['%mautic.mailer_password%'],
                ],
            ],

By:

'mautic.transport.postmark' => [
                'class' => 'Mautic\EmailBundle\Swiftmailer\Transport\PostmarkTransport',
                'serviceAlias' => 'swiftmailer.mailer.transport.%s',
                'arguments' => [
                    'mautic.email.model.transport_callback',
                    '%mautic.mailer_mailjet_sandbox%',
                    '%mautic.mailer_mailjet_sandbox_default_mail%',
                ],
                'methodCalls' => [
                    'setUsername' => ['%mautic.mailer_user%'],
                    'setPassword' => ['%mautic.mailer_password%'],
                ],
            ],

Add:

    'mailer_postmark_sandbox'              => false,
    'mailer_postmark_sandbox_default_mail' => null,

After:

    'mailer_mailjet_sandbox'              => false,
    'mailer_mailjet_sandbox_default_mail' => null,

** Replace all code in file: /mautic/app/bundles/EmailBundle/Swiftmailer/Transport/PostmarkTransport.php

By:

<?php

namespace Mautic\EmailBundle\Swiftmailer\Transport;

use Mautic\EmailBundle\Model\TransportCallback;
use Mautic\LeadBundle\Entity\DoNotContact;
use Symfony\Component\HttpFoundation\Request;

/**
 * Class PostmarkTransport.
 */
class PostmarkTransport extends \Swift_SmtpTransport  implements CallbackTransportInterface
{
    /**
     * @var bool
     */
    private $sandboxMode;

    /**
     * @var string
     */
    private $sandboxMail;

    /**
     * @var TransportCallback
     */
    private $transportCallback;

    /**
     * {@inheritdoc}
     */
    public function __construct(TransportCallback $transportCallback, $sandboxMode = false, $sandboxMail = '')
    {
        parent::__construct('smtp.postmarkapp.com', 587, 'tls');
        $this->setAuthMode('login');

        $this->setSandboxMode($sandboxMode);
        $this->setSandboxMail($sandboxMail);

        $this->transportCallback = $transportCallback;
    }

    /**
     * @param null $failedRecipients
     *
     * @return int|void
     *
     * @throws \Exception
     */
    public function send(\Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
    {
        // add leadIdHash to track this email
        if (isset($message->leadIdHash)) {
            // contact leadidHeash and email to be sure not applying email stat to bcc

            $message->getHeaders()->removeAll('X-MJ-CUSTOMID');

            $message->getHeaders()->addTextHeader('X-MJ-CUSTOMID', $message->leadIdHash.'-'.key($message->getTo()));
        }

        if ($this->isSandboxMode()) {
            $message->setSubject(key($message->getTo()).' - '.$message->getSubject());
            $message->setTo($this->getSandboxMail());
        }

        return parent::send($message, $failedRecipients);
    }


    /**
     * Returns a "transport" string to match the URL path /mailer/{transport}/callback.
     *
     * @return mixed
     */
    public function getCallbackPath()
    {
        return 'postmark';
    }

    /**
     * Handle response.
     *
     * @return mixed
     */
    public function processCallbackRequest(Request $request)
    {
        $postData = json_decode($request->getContent(), true);

        if (is_array($postData)) {

            if (!in_array($postData['RecordType'], ['Bounce', 'SpamComplaint', 'SubscriptionChange'])) {
                return false;
            }

            if ('Bounce' === $postData['RecordType']) {
                $reason = $postData['Name'].': '.$postData['Description'];
                $type   = DoNotContact::BOUNCED;
                    echo "bounce";
            } elseif ('SpamComplaint' === $postData['RecordType']) {
                $reason = $postData['Name'].': '.$postData['Description'];
                $type   = DoNotContact::UNSUBSCRIBED;
            } elseif ('SubscriptionChange' === $postData['RecordType']) {
                $reason = 'User unsubscribed';
                $type   = DoNotContact::UNSUBSCRIBED;
            } else {
                return false;
            }

            $this->transportCallback->addFailureByAddress($postData['Email'], $reason, $type);

        } else {
            // respone must be an array
            return null;
        }
    }

    /**
     * @return bool
     */
    private function isSandboxMode()
    {
        return $this->sandboxMode;
    }

    /**
     * @param bool $sandboxMode
     */
    private function setSandboxMode($sandboxMode)
    {
        $this->sandboxMode = $sandboxMode;
    }

    /**
     * @return string
     */
    private function getSandboxMail()
    {
        return $this->sandboxMail;
    }

    /**
     * @param string $sandboxMail
     */
    private function setSandboxMail($sandboxMail)
    {
        $this->sandboxMail = $sandboxMail;
    }
}
1 Like

@Nick_J I’m not sure if Postmark changed something in their webhooks payload, but the code didn’t catch up with SubscriptionChange events.

After digging a bit, I’ve noticed that in SubscriptionChange event, the payload doesn’t have the Email but uses Recepient for passing the email address.

Hence, Mautic didn’t update people who wanted to opt-out.

I’ve tweaked the processCallbackRequest() method and it works properly now. I thought it might be useful for you too. :slight_smile:

/**
     * Handle response.
     *
     * @return mixed
     */
    public function processCallbackRequest(Request $request)
    {
      error_log("Error message " . $request->getContent() . "\n", 3, "/var/www/engage.bookretreats.com/htdocs/docroot/var/logs/postmark.log");

        $postData = json_decode($request->getContent(), true);

        if (is_array($postData)) {

            if (!in_array($postData['RecordType'], ['Bounce', 'SpamComplaint', 'SubscriptionChange'])) {
                return false;
            }

            if ('Bounce' === $postData['RecordType']) {
                $email = $postData['Email'];
                $reason = $postData['Name'].': '.$postData['Description'];
                $type   = DoNotContact::BOUNCED;
                    echo "bounce";
            } elseif ('SpamComplaint' === $postData['RecordType']) {
              $email = $postData['Email'];
                $reason = $postData['Name'].': '.$postData['Description'];
                $type   = DoNotContact::UNSUBSCRIBED;
            } elseif ('SubscriptionChange' === $postData['RecordType']) {
                $email = $postData['Recipient'];
                $reason = 'User unsubscribed';
                $type   = DoNotContact::UNSUBSCRIBED;
            } else {
                return false;
            }
            //error_log("Error message " . $request->getContent() . "\n", 3, "/var/www/engage.bookretreats.com/htdocs/docroot/var/logs/postmark.log");
            $this->transportCallback->addFailureByAddress($email, $reason, $type);

        } else {
            // respone must be an array
            return null;
        }
    }

This is obsolete in 5.x not sure how to get it working