Mailgun-Compatible Webhooks
- Table of Contents
- Overview
- Configuration
- Requests
- Event types
- Field mapping
- Differences from Mailgun
Overview
An event delivery destination with the compat_mg event_delivery_format sends GreenArrow
events as Mailgun webhooks. Each request has the JSON document and the signature that Mailgun
sends, so software that was written to receive Mailgun webhooks (the “consumer”) can receive
GreenArrow events without a change.
A compat_mg destination sends only the events of messages that have the Click Tracking ID that
the Mailgun compatibility API writes. See Which messages give webhooks.
Webhooks are registered in greenarrow.conf, not through Mailgun’s Webhooks API. GreenArrow does not have a Mailgun-compatible Webhooks API.
Configuration
Mailgun lets you register one URL for each webhook type. In GreenArrow, make one event_delivery_destination for each URL. event_delivery_compat_mg_webhooks names the webhook types that go to that URL. A destination can name more than one webhook type, to send them to one URL.
For example, a consumer that has a URL for permanent failures, a URL for temporary failures and a URL for spam complaints needs these three destinations:
general {
log_header "From", "To", "Subject"
}
event_delivery_destination mailgun-permanent-failure {
event_delivery_format compat_mg
event_delivery_url https://api.example.com/webhooks/permanentFailure
event_delivery_compat_mg_signing_key </var/hvmail/control/mailgun_webhooks_signing_key>
event_delivery_compat_mg_webhooks permanent_fail
event_delivery_disable_batching yes
}
event_delivery_destination mailgun-temporary-failure {
event_delivery_format compat_mg
event_delivery_url https://api.example.com/webhooks/temporaryFailure
event_delivery_compat_mg_signing_key </var/hvmail/control/mailgun_webhooks_signing_key>
event_delivery_compat_mg_webhooks temporary_fail
event_delivery_disable_batching yes
}
event_delivery_destination mailgun-complained {
event_delivery_format compat_mg
event_delivery_url https://api.example.com/webhooks/complained
event_delivery_compat_mg_signing_key </var/hvmail/control/mailgun_webhooks_signing_key>
event_delivery_compat_mg_webhooks complained
event_delivery_disable_batching yes
}
A compat_mg destination has these rules:
- It must be an HTTP destination (event_delivery_url). The format is not valid for a logfile, pipe or SQL destination.
- event_delivery_compat_mg_signing_key is required. It is the key of the signature. The consumer must have the same key.
- event_delivery_compat_mg_webhooks is required.
-
event_delivery_disable_batching must be
yes. A Mailgun webhook has one event. - event_delivery_events and event_delivery_delivery_attempt_config are not permitted. The webhook types select the events.
-
event_delivery_anonymize_clicktrackingid and
event_delivery_anonymize_localparts are not permitted. This includes the value
no. GreenArrow builds each webhook from the Click Tracking ID, and the consumer matches the webhook to its records by the recipient. - The global log_header directive (in the
generalcontext) must name the headersFrom,ToandSubject. The names are compared without regard to case. Alog_headerin asourcecontext does not count. log_header_remove does not count, because it removes the header from the message. The webhook copies these headers from thedelivery_attemptevent. Note that this directive logs these headers for all mail on the server.
A configuration that breaks one of these rules does not load. The error names the destination and the rule.
Events of each webhook type
Each webhook type selects these GreenArrow events for the destination:
| Webhook type | GreenArrow events |
|---|---|
accepted |
None. GreenArrow does not send accepted webhooks in this version. |
delivered |
delivery_attempt with the status success
|
permanent_fail |
delivery_attempt with the status failure or failure_toolong; bounce_all.engine
|
temporary_fail |
delivery_attempt with the status deferral
|
complained |
scomp.engine |
opened |
engine_open |
clicked |
engine_click |
unsubscribed |
engine_unsub |
- A destination gets a
delivery_attemptevent only for the statuses in this table. The defaults of event_delivery_delivery_attempt_config do not apply. For example, atemporary_faildestination gets each deferral of a message, not only the first. Aconnmaxoutdelivery attempt and a delivery to a local mailbox give no webhook. - A
permanent_faildestination gets eachbounce_allevent, but sends a webhook only for an asynchronous bounce (a bounce message that arrived after the delivery). A synchronous bounce gives no webhook, because itsdelivery_attemptevent gave one. So each failed delivery gives one webhook. - The GreenArrow Studio forms of the events are not selected.
Which messages give webhooks
A compat_mg destination sends the events of a message only when the message’s Click Tracking ID
is a JSON object that has a string member m. The Mailgun compatibility API writes this Click
Tracking ID for each message that it accepts. GreenArrow checks only the Click Tracking ID, so a
message from another source that has this Click Tracking ID also gives webhooks. These are its
members:
| Member | Value |
|---|---|
m |
A unique ID of the message |
M |
The Message-ID header of the message |
t |
The tags of the message (o:tag), as a list of strings |
v |
The user variables of the message (v: values), as a JSON object |
GreenArrow removes the events of other messages from the destination’s queue and does not send
them, because the consumer cannot match them to its records. This includes GreenArrow Studio
campaigns, messages with a Click Tracking ID that is not such an object, and messages with no
Click Tracking ID. A Click Tracking ID whose M, t or v has another type than the table shows
is also not such an object.
The events of other messages go into the destination’s queue before GreenArrow removes them. If your server also sends mail outside of the Mailgun compatibility API, you can use event_delivery_filter_mail_class to keep the events of that mail out of the queue.
Requests
Each request is an HTTP POST of one event, with the Content-Type application/json. The JSON
document has two members: signature and event-data.
{
"signature": {
"timestamp": "1529006854",
"token": "a8ce0edb2dd8301dee6c2405235584e45aa91d1e9f979f3de0",
"signature": "464b0644ffb4690f66615f470cd8e1f911824e8b1ce9b7f87cc0f964e99f1fa2"
},
"event-data": {
"event": "failed",
"severity": "permanent",
"id": "8a0f9b02-5ea1-4b8f-8d9e-2c1b3a5e7d64",
"timestamp": 1529006851.123,
"recipient": "[email protected]",
"recipient-domain": "example.com",
"envelope": {
"sender": "[email protected]",
"sending-ip": "192.0.2.10"
},
"delivery-status": {
"code": 550,
"enhanced-code": "5.1.1",
"message": "192.0.2.25 does not like recipient./Remote host said: 550 5.1.1 The email account that you tried to reach does not exist/Giving up on 192.0.2.25./",
"mx-host": "mx.example.com",
"attempt-no": 1
},
"message": {
"headers": {
"message-id": "[email protected]",
"from": "Sample Sender <[email protected]>",
"to": "[email protected]",
"subject": "Your order has shipped"
},
"size": 2048
},
"user-variables": {"customer_id": "1234"},
"tags": ["shipping"]
}
}
Verify the signature
The signature object lets the consumer verify that the request comes from GreenArrow:
-
timestampis the time when GreenArrow sent the request (not the time of the event), in seconds past the Unix epoch, as a string. -
tokenis a random string of 50 hexadecimal characters. -
signatureis the hexadecimal HMAC-SHA256 oftimestampfollowed bytoken. The key is the destination’s event_delivery_compat_mg_signing_key.
This is the same signature that Mailgun sends, so a consumer that verifies Mailgun’s signature
needs no change. To verify a request, compute the HMAC-SHA256 with the same key and compare it to
signature.
For example, with the signing key key-abc123 and the request above, this command writes the
digest 464b0644ffb4690f66615f470cd8e1f911824e8b1ce9b7f87cc0f964e99f1fa2, which is equal to
signature:
printf '%s' '1529006854a8ce0edb2dd8301dee6c2405235584e45aa91d1e9f979f3de0' | openssl dgst -sha256 -hmac 'key-abc123'
The same check in Python:
import hashlib
import hmac
def verify(signing_key, timestamp, token, signature):
expected = hmac.new(
key=signing_key.encode(),
msg=(timestamp + token).encode(),
digestmod=hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Each request has a new timestamp, token and signature. This includes a retry. So a consumer
can keep the tokens that it receives, and refuse a request with a token that it received before.
A destination has one signing key. To change the key, change it in greenarrow.conf and in the
consumer.
Responses
| Response | Result |
|---|---|
2xx |
Success. GreenArrow removes the event from the destination’s queue. |
406 |
The consumer refused the event. GreenArrow removes the event from the queue and does not send it again. |
| Any other status, or a network error | Failure. GreenArrow sends the event again later. |
- Mailgun uses
406to refuse an event. On a destination with another format,406is a failure, and GreenArrow sends the event again later. - GreenArrow writes a refused event to the
event processor log.
The event delivery metrics count it as a failed delivery (
event_delivery_failed_total). - A
compat_mgdestination does not follow a redirect. A3xxresponse is a failure, and GreenArrow sends the event again later. A redirect can change thePOSTto aGETthat does not have the webhook.
Event types
| GreenArrow event | Mailgun event
|
severity |
Webhook type |
|---|---|---|---|
delivery_attempt with the status success
|
delivered |
delivered |
|
delivery_attempt with the status failure
|
failed |
permanent |
permanent_fail |
delivery_attempt with the status failure_toolong
|
failed |
permanent |
permanent_fail |
delivery_attempt with the status deferral
|
failed |
temporary |
temporary_fail |
bounce_all, asynchronous |
failed |
permanent |
permanent_fail |
scomp |
complained |
complained |
|
engine_open |
opened |
opened |
|
engine_click |
clicked |
clicked |
|
engine_unsub |
unsubscribed |
unsubscribed |
- The
severityof a delivery attempt comes from its status, not from the SMTP status code. In Mailgun,temporarymeans that the message will be tried again. GreenArrow tries a message again after a deferral. It does not try it again after afailureor afailure_toolong(the message expired in the queue), even when the SMTP status code starts with4. - An asynchronous bounce is always
permanent, because GreenArrow does not try the message again. The GreenArrow bounce type does not change the severity. It givesdelivery-status.bounce-type. - GreenArrow does not send these Mailgun events:
accepted(in this version),rejected,stored, and the mailing list events.
Suppressed events
GreenArrow removes these events from a compat_mg destination’s queue without a request, as if
they were delivered. They are not counted as delivered or failed, and are not sent again:
- An event that has no webhook in the table above: a synchronous
bounce_allevent, or adelivery_attemptevent with the statusconnmaxout. - An event of a webhook type that the destination does not name in event_delivery_compat_mg_webhooks. The destination does not select these events, so they usually do not get into its queue.
- An event of a message whose Click Tracking ID is not the one that the Mailgun compatibility API writes (see Which messages give webhooks).
Field mapping
Mailgun event-data field |
Value |
|---|---|
event |
From the event type |
severity |
temporary or permanent, from the event type; only on failed
|
id |
event_unique_id |
timestamp |
The time of the event, in seconds past the Unix epoch: timestamp of a delivery attempt (it has fractions of a second), else event_time
|
recipient |
email |
recipient-domain |
The domain of email
|
envelope.sender |
sender, also when it is empty |
envelope.sending-ip |
outmtaid_ip |
delivery-status.code |
The SMTP reply code in the status text, as a number |
delivery-status.enhanced-code |
The enhanced status code in the status text, as a string |
delivery-status.message |
The status text |
delivery-status.mx-host |
mx_hostname of a delivery attempt |
delivery-status.attempt-no |
attempt_number of a delivery attempt |
delivery-status.bounce-type |
From bounce_type of an asynchronous bounce: h gives hard; s and o give soft
|
message.headers.message-id |
M of the Click Tracking ID, else the logged Message-ID header; without the angle brackets |
message.headers.from |
The logged From header |
message.headers.to |
The logged To header |
message.headers.subject |
The logged Subject header |
message.size |
message_size |
url |
click_url, only on clicked
|
ip |
engine_ip (the IP address of the recipient), only on opened and clicked
|
client-info.user-agent |
user_agent, only on opened and clicked
|
user-variables |
v of the Click Tracking ID |
tags |
t of the Click Tracking ID |
The GreenArrow fields are described in Types of Events. These rules apply:
- A field is left out when the event has no value for it. An object with no members (
envelope,delivery-status,message,message.headersorclient-info) is left out. -
envelope.senderis an exception. It is present on eachdelivery_attemptandbounce_allevent that has asender, also when the sender is empty: the null sender (<>) is a valid envelope sender. Abounce_allevent hassenderandoutmtaid_ip, and ascompevent hasoutmtaid_ip, only when include_mtaid_info_at_delivery was enabled when the message was delivered. -
user-variablesandtagsare always present. When the Click Tracking ID has nov,user-variablesis{}. When it has not,tagsis[]. -
timestampalways has a fractional part, for example1529006854.0. - The status text is the
messageof a delivery attempt. On an asynchronous bounce, it is thebounce_textbefore the copy of the original message, without the white space at its start and end. The copy starts at the qmail line--- Below this line is a copy of the message, at a MIME part with the content typemessage/rfc822ortext/rfc822-headers, or at the Exchange lineOriginal message headers:. So the original message is not sent to the consumer, and a number in it is not read as a status code. -
delivery-status.codeis the first three-digit SMTP reply code (2xx,4xxor5xx) in the status text that is followed by a space, a hyphen or the end of the text.delivery-status.enhanced-codeis the first enhanced status code (for example5.1.1) in the status text. A number that is part of a longer run of digits and periods, such as an IP address, is not a code. When the status text has no code, the field is left out. For example, the text550 5.1.1 The email account that you tried to reach does not existgives"code": 550and"enhanced-code": "5.1.1". -
delivery-status.attempt-nois the number of the delivery attempt of the message to this recipient, starting at 1. Seeattempt_numberin Types of Events. -
delivery-status.bounce-typeis left out whenbounce_typehas another value or no value. -
message.headers.message-idis never GreenArrow’smsguid. The white space at the start and end of the value is removed, and then one<at the start and one>at the end: theMessage-IDheader<[email protected]>gives[email protected], as in Mailgun. -
message.headers.from,toandsubjectare the values of the headers, as Mailgun sends them.fromincludes the display name, for exampleSample Sender <[email protected]>. These fields are also an exception to the first rule: a logged header with an empty value gives an empty string. When a header occurs more than one time, the first value is used. A folded header is unfolded, and its encoded words (RFC 2047) are decoded. When the header has an encoded word in a character set other than UTF-8, ISO-8859-1 or US-ASCII, no encoded word of the header is decoded. - Only
delivery_attemptevents have logged headers. Sofrom,toandsubjectare only ondeliveredand on afailedwebhook of a delivery attempt. The other webhooks do not have them. - These Mailgun fields are left out:
reason,log-level,flags,geolocation,campaigns,envelope.targetsandenvelope.transport.
Differences from Mailgun
-
complained, andfailedfrom an asynchronous bounce, have nomessage.headers.from,toorsubject. Mailgun sends them on these events. GreenArrow does not log headers on these events. (Mailgun also sends onlymessage-idonopened,clickedandunsubscribed.) - The fields in the last rule of Field mapping are left out.
- GreenArrow does not send some Mailgun events. See Event types.
- Webhooks are registered in
greenarrow.conf. There is no Mailgun-compatible Webhooks API.
