Easy integration with PowerMTA accounting data

Postmastery’s webhook for PowerMTA allows for easy integration with accounting data. It provides real-time information on email sent through PowerMTA to your application. Because it uses common interface standards HTTP, REST, and JSON it works with any web framework or programming language.

PowerMTA accounting data is passed unfiltered to the webhook endpoint. Except that the CSV format is converted to JSON format. By using the PowerMTA configuration it’s easy to configure which record types and which record fields are passed to the endpoint. See section 11.2 in the PowerMTA User’s Guide for a complete list of available types and fields.

Accounting records are buffered and posted every second, or when the buffer size reaches 1MB (one megabyte), whichever occurs first. This reduces the overhead of HTTP roundtrips. It also provides durability since data will be re-posted from the buffer until a success response is received.

The webhook is implemented in native code and runs on Linux and Windows. It is spawned by PowerMTA and runs as a child process receiving accounting data in real-time. Being fast and lightweight it does not affect the performance or the stability of PowerMTA.

PowerMTA webhook examples

POSTs have a content-type header of application/json, and contain a JSON array of length 1 or more. Following a sample body with a delivered record:

[ { “type”: “d”, “timeLogged”: “1382377788”, “rcpt”: “info@postmastery.net”, “dsnAction”: “relayed”, “dsnStatus”: “2.0.0 (success)”, “dsnDiag”: “smtp;250 2.0.0 OK br19si5109139wib.83 - gsmtp”, “bounceCat”: "“, ”jobId“: ”" } ]

With a bounce record:

[ { “type”: “b”, “timeLogged”: “1382377561”, “rcpt”: “err.hot.unk@sendertools.com”, “dsnAction”: “failed”, “dsnStatus”: “5.0.0 (undefined status)”, “dsnDiag”: “smtp;550 Requested action not taken: mailbox unavailable”, “bounceCat”: “bad-mailbox”, “jobId”: "" } ]

With a remote bounce record:

[ { “type”: “rb”, “timeLogged”: “1382380477”, “rcpt”: “bou.pos.unk@sendertools.com”, “dsnAction”: “failed”, “dsnStatus”: “5.1.1”, “dsnDiag”: “xunix;User unknown”, “bounceCat”: “bad-mailbox”, “jobId”: "“, ”header_X-job“: ”" } ]

In the examples above the directive iso-times was set to no. Note that the unix timestamps are passed as string, and not as number. This is because the webhook passes all parameter values as string.

How to deploy the webhook in PowerMTA

In the following description, it is assumed that the webhook is used to pass bounce information to the endpoint. This includes the record types “b” for local (synchronous) bounces and “rb” for remote (asynchronous) bounces. The records fields are limited to the ones that are relevant for processing the bounces. The X-Job header is used to identify the mailing or mailing list.

Linux

Configure the webhook in PowerMTA using the following sample configuration:

<acct-file |/opt/pmta/acctwebhook -url http://path/to/your/webapp>
 records b,rb # record types to include (default d,b)
 iso-times no # use seconds since epoch (default yes)
 record-fields b timeLogged,rcpt,dsnAction,dsnStatus,dsnDiag,bounceCat,jobId
 record-fields rb timeLogged,rcpt,dsnAction,dsnStatus,dsnDiag,bounceCat,header_X-Job
</acct-file>

Windows

Configure the webhook in PowerMTA using the following sample configuration:

<acct-file |c:\pmta\bin\acctwebhook -url http://path/to/your/webapp>
  records b,rb # record types to include (default d,b)
  iso-times no # use seconds since epoch (default yes)
  record-fields b timeLogged,rcpt,dsnAction,dsnStatus,dsnDiag,bounceCat,jobId
  record-fields rb timeLogged,rcpt,dsnAction,dsnStatus,dsnDiag,bounceCat,header_X-Job
</acct-file>

Sample implementations of PowerMTA webhook endpoints in PHP and Ruby

This section contains sample implementations of webhook endpoints.

PHP

The following example shows how to implement an endpoint in PHP:

<?php
$body = file_get_contents("php://input");
$data = json_decode($body, true);
foreach ($data as $record) {
  echo $record["type"];
  // ...
}

Ruby

The following example shows how to implement an endpoint in Ruby using Sinatra:

require 'sinatra'
require 'json'
post "/logdata" do
  data = JSON.parse(request.body.read)
  if data
    data.each do |record|
      # ...
    end
    status 200
  else
    status 400
  end
end

Would you like to assess or improve your deliverability?