# Webhook Postmark support: management of bounces, spam and unsubscription

**URL:** <https://forum.mautic.org/t/webhook-postmark-support-management-of-bounces-spam-and-unsubscription/25799>\
**Category:** General Discussion\
**Created:** [October 23, 2022, 9:07pm UTC](https://forum.mautic.org/t/webhook-postmark-support-management-of-bounces-spam-and-unsubscription/25799 "2022-10-23T21:07:57Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![Nick\_J](https://sea2.discourse-cdn.com/flex020/user_avatar/forum.mautic.org/nick_j/32/8369_2.png) [@Nick\_J](https://forum.mautic.org/u/Nick_J)\
**Post date:** [October 23, 2022, 9:07pm UTC](https://forum.mautic.org/t/webhook-postmark-support-management-of-bounces-spam-and-unsubscription/25799/1 "2022-10-23T21:07:57Z")

</div>

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:**

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

```

**By:**

```auto
'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:**

```auto
<?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;
    }
}

```

---

<div class="post-metadata">

**Author:** ![goranculibrk](https://sea2.discourse-cdn.com/flex020/user_avatar/forum.mautic.org/goranculibrk/32/127_2.png) [@goranculibrk](https://forum.mautic.org/u/goranculibrk)\
**Post date:** [February 22, 2023, 9:38am UTC](https://forum.mautic.org/t/webhook-postmark-support-management-of-bounces-spam-and-unsubscription/25799/2 "2023-02-22T09:38:07Z")

</div>

@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. 🙂

```auto
/**
     * 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;
        }
    }

```

---

<div class="post-metadata">

**Author:** ![av1dlearner](https://sea2.discourse-cdn.com/flex020/user_avatar/forum.mautic.org/av1dlearner/32/997_2.png) [@av1dlearner](https://forum.mautic.org/u/av1dlearner)\
**Post date:** [January 12, 2024, 6:51pm UTC](https://forum.mautic.org/t/webhook-postmark-support-management-of-bounces-spam-and-unsubscription/25799/3 "2024-01-12T18:51:49Z")

</div>

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

> [@Mautic 5 New callback process results in 404](https://forum.mautic.org/t/mautic-5-new-callback-process-results-in-404/30479):
>
> Your software My Mautic version is: 5.0.1 My PHP version is: 8.1.27 My Database type and version is: Your problem Hi everyone, I upgraded to 5.0.1 clap but I saw that the callback process is now unified under [https://mautic.example.com/mailer/callback](https://mautic.example.com/mailer/callback) however my postmark webhook calls will get a 404 These errors are showing in the log: "POST /mailer/callback HTTP/1.1" 404 5592 "-" "Postmark" "POST /mailer/callback HTTP/1.1" 404 5592 "-" "Postmark"
