Mail Transfer Agent (MTA) Hooks: An HTTP-Based Protocol for External Mail Processing
draft-degennaro-mta-hooks-02
This document is an Internet-Draft (I-D).
Anyone may submit an I-D to the IETF.
This I-D is not endorsed by the IETF and has no formal standing in the
IETF standards process.
| Document | Type | Active Internet-Draft (individual) | |
|---|---|---|---|
| Author | Mauro De Gennaro | ||
| Last updated | 2026-10-28 | ||
| RFC stream | (None) | ||
| Intended RFC status | (None) | ||
| Formats | |||
| Stream | Stream state | (No stream defined) | |
| Consensus boilerplate | Unknown | ||
| RFC Editor Note | (None) | ||
| IESG | IESG state | I-D Exists | |
| Telechat date | (None) | ||
| Responsible AD | (None) | ||
| Send notices to | (None) |
draft-degennaro-mta-hooks-02
Network Working Group M. De Gennaro
Internet-Draft Stalwart Labs
Intended status: Standards Track 28 October 2026
Expires: 1 May 2027
Mail Transfer Agent (MTA) Hooks: An HTTP-Based Protocol for External
Mail Processing
draft-degennaro-mta-hooks-02
Abstract
This document specifies MTA Hooks, an HTTP-based protocol that lets
Mail Transfer Agents (MTAs) delegate message processing decisions to
external services. The MTA sends a JSON or Concise Binary Object
Representation (CBOR) description of the SMTP session, envelope, and
message to a registered service at defined stages of reception and
delivery. The service answers with a JSON Patch that changes the
description, and the MTA acts on the resulting state. The same
mechanism covers filtering at SMTP time, routing before delivery, and
reacting to delivery outcomes, and the MTA controls exactly which
changes each service may make.
Status of This Memo
This Internet-Draft is submitted in full conformance with the
provisions of BCP 78 and BCP 79.
Internet-Drafts are working documents of the Internet Engineering
Task Force (IETF). Note that other groups may also distribute
working documents as Internet-Drafts. The list of current Internet-
Drafts is at https://datatracker.ietf.org/drafts/current/.
Internet-Drafts are draft documents valid for a maximum of six months
and may be updated, replaced, or obsoleted by other documents at any
time. It is inappropriate to use Internet-Drafts as reference
material or to cite them other than as "work in progress."
This Internet-Draft will expire on 1 May 2027.
Copyright Notice
Copyright (c) 2026 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents (https://trustee.ietf.org/
license-info) in effect on the date of publication of this document.
De Gennaro Expires 1 May 2027 [Page 1]
Internet-Draft MTA Hooks October 2026
Please review these documents carefully, as they describe your rights
and restrictions with respect to this document. Code Components
extracted from this document must include Revised BSD License text as
described in Section 4.e of the Trust Legal Provisions and are
provided without warranty as described in the Revised BSD License.
Table of Contents
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1. Notational Conventions . . . . . . . . . . . . . . . . . 4
1.2. Terminology . . . . . . . . . . . . . . . . . . . . . . . 4
1.3. Data Types . . . . . . . . . . . . . . . . . . . . . . . 4
2. Protocol Overview . . . . . . . . . . . . . . . . . . . . . . 5
2.1. Architecture . . . . . . . . . . . . . . . . . . . . . . 5
2.2. Serialization . . . . . . . . . . . . . . . . . . . . . . 6
2.3. Stages . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.4. Scanner Chains . . . . . . . . . . . . . . . . . . . . . 7
2.5. State Changes . . . . . . . . . . . . . . . . . . . . . . 7
3. Discovery . . . . . . . . . . . . . . . . . . . . . . . . . . 7
4. Registration . . . . . . . . . . . . . . . . . . . . . . . . 9
4.1. Registration Request . . . . . . . . . . . . . . . . . . 9
4.1.1. Filters . . . . . . . . . . . . . . . . . . . . . . . 10
4.1.2. Grants . . . . . . . . . . . . . . . . . . . . . . . 11
4.2. Registration Response . . . . . . . . . . . . . . . . . . 11
4.3. Authentication . . . . . . . . . . . . . . . . . . . . . 14
4.4. Registration Lifecycle . . . . . . . . . . . . . . . . . 14
5. Hook Request . . . . . . . . . . . . . . . . . . . . . . . . 15
5.1. HTTP Request . . . . . . . . . . . . . . . . . . . . . . 15
5.2. Properties . . . . . . . . . . . . . . . . . . . . . . . 15
5.3. Session . . . . . . . . . . . . . . . . . . . . . . . . . 17
5.4. Envelope and Recipients . . . . . . . . . . . . . . . . . 19
5.5. Message . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.6. Sender Authentication . . . . . . . . . . . . . . . . . . 22
5.7. Queue . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.8. Extension Data . . . . . . . . . . . . . . . . . . . . . 23
5.9. SMTP Response . . . . . . . . . . . . . . . . . . . . . . 23
5.10. EHLO Capabilities . . . . . . . . . . . . . . . . . . . . 24
5.11. Examples . . . . . . . . . . . . . . . . . . . . . . . . 24
6. Hook Response . . . . . . . . . . . . . . . . . . . . . . . . 26
6.1. Structure . . . . . . . . . . . . . . . . . . . . . . . . 26
6.2. Validation and Application . . . . . . . . . . . . . . . 27
6.3. Ordering . . . . . . . . . . . . . . . . . . . . . . . . 27
6.4. Actions . . . . . . . . . . . . . . . . . . . . . . . . . 28
6.5. Recipient Changes . . . . . . . . . . . . . . . . . . . . 30
6.6. Routing . . . . . . . . . . . . . . . . . . . . . . . . . 31
6.7. Transmission . . . . . . . . . . . . . . . . . . . . . . 32
6.8. Examples . . . . . . . . . . . . . . . . . . . . . . . . 32
6.9. Error Responses . . . . . . . . . . . . . . . . . . . . . 33
De Gennaro Expires 1 May 2027 [Page 2]
Internet-Draft MTA Hooks October 2026
7. Transport . . . . . . . . . . . . . . . . . . . . . . . . . . 34
7.1. HTTP and TLS . . . . . . . . . . . . . . . . . . . . . . 34
7.2. Retries . . . . . . . . . . . . . . . . . . . . . . . . . 34
7.3. Failure Handling . . . . . . . . . . . . . . . . . . . . 34
8. Implementation Status . . . . . . . . . . . . . . . . . . . . 35
9. Security Considerations . . . . . . . . . . . . . . . . . . . 35
9.1. Trust Model . . . . . . . . . . . . . . . . . . . . . . . 35
9.2. Authority of Scanners . . . . . . . . . . . . . . . . . . 35
9.3. Credentials . . . . . . . . . . . . . . . . . . . . . . . 36
9.4. Denial of Service . . . . . . . . . . . . . . . . . . . . 36
9.5. Injection . . . . . . . . . . . . . . . . . . . . . . . . 36
9.6. Unverified Data . . . . . . . . . . . . . . . . . . . . . 36
10. Privacy Considerations . . . . . . . . . . . . . . . . . . . 37
11. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 37
11.1. Well-Known URI . . . . . . . . . . . . . . . . . . . . . 37
11.2. HTTP Field Names . . . . . . . . . . . . . . . . . . . . 37
11.3. MTA Hooks Registries . . . . . . . . . . . . . . . . . . 37
11.3.1. MTA Hooks Stages . . . . . . . . . . . . . . . . . . 38
11.3.2. MTA Hooks Actions . . . . . . . . . . . . . . . . . 38
11.3.3. MTA Hooks Session Counters . . . . . . . . . . . . . 39
11.3.4. MTA Hooks Error Codes . . . . . . . . . . . . . . . 40
12. References . . . . . . . . . . . . . . . . . . . . . . . . . 40
12.1. Normative References . . . . . . . . . . . . . . . . . . 40
12.2. Informative References . . . . . . . . . . . . . . . . . 44
Appendix A. Error Codes . . . . . . . . . . . . . . . . . . . . 46
Appendix B. Acknowledgments . . . . . . . . . . . . . . . . . . 47
Appendix C. Changes . . . . . . . . . . . . . . . . . . . . . . 47
Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 49
1. Introduction
Mail Transfer Agents need extensible mechanisms to integrate external
services for spam filtering, virus scanning, policy enforcement,
routing, and compliance. Existing mechanisms each cover a part of
the problem: content filter protocols such as Milter [MILTER] see
only message reception, lookup tables receive a single key, and
delivery outcomes are mostly recovered by parsing logs.
MTA Hooks covers the whole life of a message with one protocol. At
each processing stage the MTA describes the current state as a single
JavaScript Object Notation (JSON) [RFC8259] object: the SMTP session,
the envelope with the status of every recipient, the message, and the
delivery attempts. A scanner answers with a JSON Patch [RFC6902]
against that object. Accepting, rejecting, adding a header,
discarding a recipient, choosing a route, or suppressing a delivery
status notification (DSN) [RFC3461] are all expressed the same way:
as a change to the state.
De Gennaro Expires 1 May 2027 [Page 3]
Internet-Draft MTA Hooks October 2026
The MTA decides which parts of the object each scanner may change,
and by default a scanner may only make processing stricter. Making
it more permissive, such as overriding a rejection or lowering a TLS
requirement, has to be granted explicitly by the MTA operator.
The protocol uses ordinary HTTP [RFC9110], so existing client and
server libraries, load balancers, proxies, and monitoring systems
work unchanged.
1.1. Notational Conventions
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
capitals, as shown here.
1.2. Terminology
MTA: A Mail Transfer Agent as described in [RFC5321].
Scanner: An HTTP service that receives hook requests from an MTA and
returns changes.
Registration: The state, held by the scanner, that records which
stages, properties, and changes an MTA has agreed to exchange with
it.
Subscription: A part of a registration that applies to the messages
and stages matching its filter.
Stage: A point in mail processing at which the MTA invokes scanners.
Hook request: The object describing the current state that the MTA
sends to a scanner.
Grant: A permission, given by the MTA, to change a part of the hook
request.
Property path: A JSON Pointer [RFC6901] into the hook request, in
which the reference token "*", used in place of an array index,
matches any index.
1.3. Data Types
String: A JSON string.
UnsignedInt: An integer in the range 0 <= value <= 2^53-1.
De Gennaro Expires 1 May 2027 [Page 4]
Internet-Draft MTA Hooks October 2026
Boolean: A JSON boolean.
UTCDate: A "date-time" string as defined in [RFC3339] whose time-
offset is "Z".
Bytes: Binary data. In JSON it is a base64 string (Section 4 of
[RFC4648]); in CBOR it is a byte string.
A[] denotes an array of A, and String[A] denotes an object whose keys
are strings and whose values are of type A. When sent, a property
whose type includes null MAY be omitted, which is equivalent to null,
and a property with a stated default MAY be omitted, which is
equivalent to the default.
2. Protocol Overview
2.1. Architecture
The MTA is always the HTTP client. It discovers a scanner, registers
with it, and then sends it hook requests during mail processing. The
MTA exposes no HTTP endpoint.
+-----+ GET /.well-known/mta-hooks +---------+
| |-------------------------------------->| |
| | POST registration | |
| MTA |-------------------------------------->| Scanner |
| | POST hook request | |
| |-------------------------------------->| |
| |<--------------------------------------| |
+-----+ JSON Patch +---------+
Figure 1: MTA Hooks Protocol Flow
1. Optionally, the MTA retrieves the scanner's discovery document
(Section 3).
2. The MTA registers, stating the stages, properties, and grants for
each subscription (Section 4).
3. At each subscribed stage the MTA sends a hook request
(Section 5).
4. The scanner returns a JSON Patch, which the MTA validates against
the grants and applies (Section 6).
De Gennaro Expires 1 May 2027 [Page 5]
Internet-Draft MTA Hooks October 2026
2.2. Serialization
Discovery, registration, and error responses use JSON. Hook requests
and responses use JSON or CBOR [RFC8949], chosen at registration. In
CBOR, the data model is that of JSON, with text strings as map keys,
except that Bytes values are byte strings, which avoids the size
increase of base64 for message content. JSON Pointer and JSON Patch
apply unchanged, and a "test" operation compares byte strings octet
for octet.
2.3. Stages
+==========+============================================+
| Stage | Invoked |
+==========+============================================+
| connect | When a client connects, before the |
| | greeting is sent; on a port with implicit |
| | TLS [RFC8314], after the TLS handshake. |
+----------+--------------------------------------------+
| ehlo | After each EHLO or HELO command, including |
| | the one that follows STARTTLS. |
+----------+--------------------------------------------+
| mail | After each MAIL command. |
+----------+--------------------------------------------+
| rcpt | After each RCPT command. |
+----------+--------------------------------------------+
| data | After the message content has been |
| | received, before the final reply. |
+----------+--------------------------------------------+
| route | Each time the MTA takes recipients from |
| | the queue for a delivery attempt, before |
| | resolving or connecting to the next hop. |
+----------+--------------------------------------------+
| transmit | Before each SMTP transaction of a delivery |
| | attempt, once its envelope is final and |
| | before the message is transmitted. |
+----------+--------------------------------------------+
| delivery | After the delivery attempts of a delivery |
| | job have completed. |
+----------+--------------------------------------------+
| dsn | Before the MTA sends a delivery status |
| | notification it generated. |
+----------+--------------------------------------------+
Table 1: Stages
De Gennaro Expires 1 May 2027 [Page 6]
Internet-Draft MTA Hooks October 2026
The first five stages are inbound stages and the last four are
outbound stages. Each inbound stage request carries the state
accumulated so far in the session, subject to the subscription's
properties, so a scanner that subscribes only to "data" receives the
connection, TLS, authentication, envelope, and message in a single
request and can still reject the message at SMTP time.
A delivery job is a set of recipients of one message that the MTA
attempts in one processing cycle. How recipients are grouped into
jobs is implementation-defined, except as constrained by routing
(Section 6.6).
2.4. Scanner Chains
When several registrations subscribe to the same stage, the MTA
invokes them sequentially in a configured order. Each scanner
receives the state as changed by the previous scanners, including the
current action and which scanner set it. The chain ends early when a
scanner sets a terminal action (Section 6.4).
Each registration is identified in hook requests by a label that the
MTA operator assigns to it, for example "spam" or "archive", and that
the MTA sends in the registration request. The label is not chosen
by the scanner, is stable across re-registration, and is unique
within the MTA. It has the syntax of the registration identifier
(Section 5.1), with at most 64 characters. The reserved label "mta"
denotes the MTA itself.
2.5. State Changes
A scanner changes processing by returning a JSON Patch [RFC6902] that
is applied to the hook request. The MTA then acts on the resulting
state. There is no other way to influence processing, so every
effect a scanner can have corresponds to a path in the hook request,
and every permission is a grant on a path.
3. Discovery
A scanner MAY publish a discovery document at "/.well-known/mta-
hooks". The MTA retrieves it with GET. Discovery is optional: the
MTA may instead be configured with the registration URL.
The discovery document has the following properties:
version: String The protocol version, "1.0" for this specification.
An MTA MUST NOT register with a scanner whose version it does not
support.
De Gennaro Expires 1 May 2027 [Page 7]
Internet-Draft MTA Hooks October 2026
registration: String URL of the registration endpoint, an absolute
URL or a relative reference resolved against the URL of the
discovery document (Section 5 of [RFC3986]).
serialization: String[] Media types of the supported serializations
for hook requests: "application/json", "application/cbor", or
both.
stages: String[] Stages the scanner can process.
properties: String[] Property paths the scanner wants to receive.
grants: Grant[] Changes the scanner may want to make
(Section 4.1.2).
maxMessageSize: UnsignedInt|null Largest message, in octets, the
scanner accepts. The MTA does not send a request for a larger
message and instead treats the invocation as a failure of the
scanner (Section 7.3).
timeoutMs: UnsignedInt|null Time, in milliseconds, the scanner needs
to answer a typical request.
{
"version": "1.0",
"registration": "/v1/registrations",
"serialization": ["application/cbor", "application/json"],
"stages": ["connect", "rcpt", "data", "route", "delivery"],
"properties": ["/session", "/envelope", "/message",
"/senderAuth", "/queue", "/ext/org.example.av"],
"grants": [
{"path": "/action",
"values": ["accept", "quarantine", "reject", "discard"]},
{"path": "/response"},
{"path": "/message/headers"},
{"path": "/envelope/to/*/status"},
{"path": "/envelope/to/*/route"},
{"path": "/ext/org.example.spam"}
],
"maxMessageSize": 52428800,
"timeoutMs": 2000
}
De Gennaro Expires 1 May 2027 [Page 8]
Internet-Draft MTA Hooks October 2026
4. Registration
The MTA registers by POSTing a registration request to the
registration endpoint. The MTA builds the request from its
configuration; it states what the MTA will send and what it will
allow. The scanner can accept it, narrow it, or refuse it, but never
widen it.
4.1. Registration Request
name: String Human-readable name of the MTA instance.
label: String The label of this registration in hook requests
(Section 2.4).
softwareVersion: String|null MTA software version.
timeoutMs: UnsignedInt Longest time, in milliseconds, the MTA waits
for a hook response.
expiresAt: UTCDate|null Requested expiration. The scanner MAY
assign a different one.
serialization: String Media type of the serialization for hook
requests and responses. A scanner that does not support it
refuses the registration with 422 UNSUPPORTED_SERIALIZATION.
subscriptions: Subscription[] The subscriptions, in order. MUST NOT
be empty.
A *Subscription* object has the following properties:
stages: String[] Stages this subscription applies to.
filter: FilterOperator|FilterCondition|null Which messages and
connections this subscription applies to (Section 4.1.1). Null
matches everything.
properties: String[] Property paths the MTA will send. A path
includes everything below it.
grants: Grant[] Changes the scanner may make (Section 4.1.2).
onFailure: String What the MTA does when the scanner fails
(Section 7.3): "continue" or "tempfail". Default "continue".
De Gennaro Expires 1 May 2027 [Page 9]
Internet-Draft MTA Hooks October 2026
For each stage, the MTA uses the first subscription, in order, that
lists the stage and whose filter matches. If none matches, the
scanner is not invoked. Different parts of the mail stream can
therefore receive different properties, permissions, and failure
handling.
At the "route" and "delivery" stages, subscriptions are matched per
recipient, with envelopeTo evaluated against that recipient alone.
When recipients of a job match different subscriptions of the same
registration, the MTA invokes the scanner once for each of those
subscriptions, in subscription order, with only the recipients it
matches, so each subscription's grants and onFailure apply only to
its own recipients. At the "transmit" and "dsn" stages the
transaction or DSN is matched as a whole, because its changes apply
to a single copy.
4.1.1. Filters
A filter uses the FilterOperator and FilterCondition syntax of
Section 5.5 of [RFC8620] with the Email filter conditions of
Section 4.4.1 of [RFC8621], except those that depend on mailbox or
thread state: inMailbox, inMailboxOtherThan, before, after,
hasKeyword, notKeyword, allInThreadHaveKeyword,
someInThreadHaveKeyword, and noneInThreadHaveKeyword.
The following conditions are added. Their String values MAY contain
"*", which matches zero or more characters. Domains are compared
case-insensitively; local parts and logins are compared exactly.
envelopeFrom: String The MAIL FROM address matches.
envelopeTo: String The address of any recipient whose status is not
"failed", "deferred", or "discarded" matches.
clientIp: String The client IP address is within the given address
or Classless Inter-Domain Routing (CIDR) prefix ([RFC4632],
Section 2.3 of [RFC4291]).
serverPort: UnsignedInt The client connected to this port.
tls: Boolean Whether the session uses TLS.
authenticated: Boolean Whether the client authenticated.
authLogin: String The authenticated login matches.
De Gennaro Expires 1 May 2027 [Page 10]
Internet-Draft MTA Hooks October 2026
A condition that refers to information not available at the stage
being evaluated, such as the subject at the "rcpt" stage or the
client address of a locally submitted message, evaluates to false, so
its negation evaluates to true.
The filter is evaluated by the MTA. It is included in the
registration so the scanner knows which traffic it will see.
4.1.2. Grants
A *Grant* object has the following properties:
path: String A property path. The grant covers the locations it
matches and everything below them.
values: String[]|null If not null, every String value at or below
the path that differs after the patch MUST match one of these
values, which MAY contain the "*" wildcard of Section 4.1.1.
max: UnsignedInt|null If not null, a number at the path MUST NOT
exceed it after the patch; any other value at the path MUST NOT
exceed it in size, in octets, in the negotiated serialization.
relax: Boolean Whether the scanner may change ordered properties in
the permissive direction (Section 6.3). Default false.
A change to a location not covered by a grant is not permitted. When
several grants cover a location, the most specific one applies: the
one whose path has the most reference tokens and, among those, the
one with a literal token where the others have "*". A narrower grant
can thus restrict part of a broader one. Examples:
[
{"path": "/message/headers"},
{"path": "/action", "relax": true},
{"path": "/envelope/to/*/route/name",
"values": ["default", "vpn-*"]},
{"path": "/response/delayMs", "max": 30000},
{"path": "/ext/org.example.spam", "max": 4096}
]
4.2. Registration Response
On success the scanner returns 201 Created with a Location header
field giving the registration URL, and a Registration object:
id: String Registration identifier, with the syntax of Section 5.1.
De Gennaro Expires 1 May 2027 [Page 11]
Internet-Draft MTA Hooks October 2026
expiresAt: UTCDate|null Expiration time, or null for none.
hookEndpoint: String URL to which hook requests are sent, an
absolute URL or a relative reference resolved against the
registration URL.
An absolute URL in "registration" or "hookEndpoint" MUST have the
same origin as the URL it is resolved against, unless the MTA's
configuration allows the other origin. The MTA MUST NOT follow
redirects for registration and hook requests; a 3xx status is a
failure.
serialization: String Confirmed serialization media type.
subscriptions: Subscription[] The subscriptions as accepted. The
scanner MAY remove properties and grants it does not need, but
MUST NOT otherwise change the subscriptions, because removing a
stage or a subscription would let traffic fall through to a later
subscription. A scanner that cannot serve a subscription refuses
the registration with 422. The MTA MUST NOT send properties or
honor changes beyond what is confirmed here.
De Gennaro Expires 1 May 2027 [Page 12]
Internet-Draft MTA Hooks October 2026
POST /v1/registrations HTTP/1.1
Host: scanner.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"name": "mx1.example.com",
"label": "spam",
"softwareVersion": "2.1.0",
"timeoutMs": 5000,
"serialization": "application/json",
"subscriptions": [
{
"stages": ["data"],
"filter": {"authenticated": false},
"properties": ["/session", "/envelope", "/message",
"/senderAuth"],
"grants": [
{"path": "/action"},
{"path": "/response"},
{"path": "/message/headers"},
{"path": "/ext/org.example.spam", "max": 4096}
],
"onFailure": "continue"
},
{
"stages": ["route"],
"filter": {"envelopeTo": "*@partner.example"},
"properties": ["/envelope", "/queue",
"/ext/org.example.spam"],
"grants": [
{"path": "/envelope/to/*/route/name",
"values": ["vpn-partner"]}
],
"onFailure": "tempfail"
}
]
}
De Gennaro Expires 1 May 2027 [Page 13]
Internet-Draft MTA Hooks October 2026
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/registrations/reg-7a3b9c2e
{
"id": "reg-7a3b9c2e",
"expiresAt": "2027-10-28T10:00:00Z",
"hookEndpoint": "/v1/hooks/reg-7a3b9c2e",
"serialization": "application/json",
"subscriptions": [ ... ]
}
In this response, which echoes the two requested subscriptions
unchanged, they are abbreviated. The HTTP examples in this document
omit Content-Length.
4.3. Authentication
The MTA MUST authenticate every request it makes to the scanner other
than retrieval of the discovery document. Implementations MUST
support bearer tokens [RFC6750] in the Authorization header field,
and MAY support TLS client certificates instead. Credentials are
issued by the scanner operator to each MTA through an out-of-band
channel. The scanner MUST reject unauthenticated requests with 401,
including a WWW-Authenticate header field as [RFC6750] requires, and
requests from an MTA not authorized for the registration with 403.
4.4. Registration Lifecycle
The MTA MAY retrieve the current Registration object with GET on the
registration URL. It deregisters with DELETE on that URL, to which
the scanner answers 204 (No Content). After deregistration the MTA
MUST NOT start new hook requests for the registration. Hook requests
already in flight MAY complete.
A registration that expires or is terminated by the scanner causes
subsequent requests to fail with 404 REGISTRATION_NOT_FOUND. The MTA
MUST treat that response on a previously active registration as a
signal to register again. The MTA SHOULD register again before
expiresAt, and when its configuration for the scanner changes. Each
registration has a new identifier; once the new registration is
active, the MTA SHOULD delete the one it replaces. A scanner SHOULD
continue to accept hook requests for a replaced registration for a
short grace period.
Scanner instances behind a load balancer MUST share registration
state or partition it by registration identifier.
De Gennaro Expires 1 May 2027 [Page 14]
Internet-Draft MTA Hooks October 2026
5. Hook Request
5.1. HTTP Request
The MTA sends each hook request with POST to the hookEndpoint, with a
Content-Type of "application/json" or "application/cbor" and the
following header fields:
MTA-Hooks-Registration: REQUIRED. The registration identifier.
MTA-Hooks-Request-Id: REQUIRED. Identifies the logical invocation.
It MUST be the same on every retry of an invocation. Scanners
SHOULD use it to deduplicate.
MTA-Hooks-Context: OPTIONAL. An opaque value that is the same for
all requests about the same session at inbound stages, and about
the same message at outbound stages, so that load balancers can
route related requests to the same scanner instance without
parsing the body. Scanners MUST NOT depend on such affinity.
Their values have the following syntax, using ABNF [RFC5234] and the
"unreserved" rule of Section 2.3 of [RFC3986]:
MTA-Hooks-Registration = 1*255( ALPHA / DIGIT / "-" / "_" / ":" )
MTA-Hooks-Request-Id = 1*128unreserved
MTA-Hooks-Context = 1*128unreserved
5.2. Properties
The hook request is an object whose properties are listed below.
"stage", "timestamp", and "timeoutMs" are always sent, and "action",
"decidedBy", and "response" are always sent at inbound stages. The
others are sent when they are covered by the subscription's
properties and available at the stage.
stage: String The current stage.
action: String Inbound stages only: what the MTA will do unless
changed (Section 6.4).
decidedBy: String Inbound stages only: label of the registration
that last changed the action, or "mta".
response: SmtpResponse Inbound stages only: the SMTP reply the MTA
will send for the current command.
timestamp: UTCDate When the stage began.
De Gennaro Expires 1 May 2027 [Page 15]
Internet-Draft MTA Hooks October 2026
timeoutMs: UnsignedInt Milliseconds the scanner has to answer this
request: the registration's timeoutMs, reduced by any budget the
MTA's configuration assigns to the stage or the chain and by time
already spent on earlier attempts of this invocation. Answers
arriving later are discarded.
traceId: String|null Opaque identifier that correlates the message
across the MTAs of an administrative domain. How it is obtained
and propagated between MTAs is outside the scope of this document.
It SHOULD be globally unique, for example a version 7 Universally
Unique Identifier (UUIDv7) [RFC9562]. A scanner with a grant MAY
set it while it is null; once set, it cannot be changed.
session: Session|null The session in which the message was received
(Section 5.3).
capabilities: String[String|null] At the "ehlo" stage, the SMTP
service extensions the MTA will announce, keyed by upper-case
keyword, with the parameters as a space-separated string or null
(Section 5.10).
envelope: Envelope Sender and recipients (Section 5.4).
message: Email The parsed message (Section 5.5).
rawMessage: Bytes The message in Internet Message Format [RFC5322].
senderAuth: SenderAuth Sender authentication results (Section 5.6).
queue: Queue The queued message (Section 5.7).
ext: String[Object] Extension data (Section 5.8).
A property is not sent when the MTA does not have its information,
such as the envelope before MAIL, the message before the end of DATA,
the queue before the MTA assigns an identifier, or the session and
senderAuth at outbound stages when the MTA did not retain them. At
the "transmit" stage, envelope and message describe the transaction
about to be performed. At the "dsn" stage, envelope, message, and
rawMessage describe the DSN about to be sent; queue identifies the
message the DSN reports on.
De Gennaro Expires 1 May 2027 [Page 16]
Internet-Draft MTA Hooks October 2026
5.3. Session
A *Session* object describes an SMTP session. For a message
submitted by other means, such as the JSON Meta Application Protocol
(JMAP) [RFC8621] or a local submission program, it describes that
submission as far as applicable; for example, "auth" identifies the
submitting account and "client" is null for local submission.
id: String Unique identifier of the session, stable across its
stages and transactions.
startedAt: UTCDate When the connection was accepted.
client: ClientInfo|null The client.
server: ServerInfo|null The address the client connected to.
tls: TlsInfo|null TLS negotiated or attempted on the session, or
null.
auth: SmtpAuth|null Authentication of the client, or null.
counters: String[UnsignedInt] Session counters and measurements,
keyed by names from the registry in Section 11.3.3. The MTA
includes those it maintains.
A *ClientInfo* object has the following properties:
ip: String Client IP address.
port: UnsignedInt Client port.
ptr: String|null Host name from the PTR record of the IP address.
ehlo: String|null Argument of the most recent EHLO or HELO command.
asn: UnsignedInt|null Autonomous System Number (ASN) of the IP
address.
country: String|null Alpha-2 country code [ISO3166-1] of the IP
address.
activeConnections: UnsignedInt|null Concurrent connections from this
IP address.
proxy: ProxyInfo|null How the client identity was conveyed, if not
taken from the socket.
De Gennaro Expires 1 May 2027 [Page 17]
Internet-Draft MTA Hooks October 2026
The client properties carry the effective client identity. When it
was conveyed by a proxy mechanism that the MTA trusts, "proxy"
describes that mechanism; when the mechanism is not trusted, the MTA
ignores it and the request contains the socket identity without a
"proxy" property.
A *ProxyInfo* object has the following properties:
protocol: String "proxy-v1" or "proxy-v2" for the PROXY protocol
[PROXY], "xclient" for XCLIENT [XCLIENT], or "xforward" for
XFORWARD [XFORWARD].
ip: String IP address of the proxy, the socket peer.
port: UnsignedInt Port of the proxy.
overridden: String[] Property paths of the session values that came
from the proxy, for example "/session/client/ip" or
"/session/server/port".
A *ServerInfo* object has the following properties:
name: String Host name the MTA uses on this connection.
ip: String IP address the client connected to.
port: UnsignedInt Port the client connected to.
A *TlsInfo* object has the following properties:
version: String|null TLS version: "TLSv1.2" or "TLSv1.3", or null if
no handshake completed.
cipher: String|null Name of the cipher suite as listed in the IANA
"TLS Cipher Suites" registry, or null if no handshake completed.
peerCertificate: Certificate|null The certificate presented by the
peer: the client certificate at inbound stages, the server
certificate at outbound stages. Null if none was presented.
policy: String|null Outbound only: the policy type applied, as
defined for "policy-type" in [RFC8460].
failure: String|null Outbound only: why TLS failed, as a "result-
type" value of [RFC8460], or null.
A *Certificate* object has the following properties:
De Gennaro Expires 1 May 2027 [Page 18]
Internet-Draft MTA Hooks October 2026
chain: Bytes[] The certificates as presented, encoded with the
Distinguished Encoding Rules (DER) [X.690], end-entity certificate
first.
verification: String "pass" if the MTA verified the certificate,
using PKIX [RFC5280] or DNS-Based Authentication of Named Entities
(DANE) [RFC7672]; "none" if it did not attempt verification;
otherwise why verification failed, as one of the "result-type"
values "certificate-expired", "certificate-not-trusted",
"certificate-host-mismatch", or "validation-failure" of [RFC8460].
The certificate is reported whether or not it could be verified. A
certificate that did not verify is an unauthenticated assertion by
the peer; it is useful for recognizing a peer, but not as proof of
its identity.
An *SmtpAuth* object has the following properties:
login: String Authenticated identity.
method: String|null Name of the Simple Authentication and Security
Layer (SASL) [RFC4422] mechanism used with SMTP AUTH [RFC4954], or
null for authentication by other means.
5.4. Envelope and Recipients
An *Envelope* object has the following properties:
from: Address The reverse-path.
to: Recipient[] The recipients.
An *Address* object has the following properties:
address: String|null The address, or null for the null reverse-path.
parameters: String[String|null] All Extended SMTP (ESMTP) parameters
given with the address, keyed by upper-case name, with null for a
parameter without a value. Values that their extension encodes as
xtext, such as ENVID and the address in ORCPT [RFC3461], are
decoded. Examples are SIZE, BODY, SMTPUTF8, REQUIRETLS, RET,
ENVID, NOTIFY, and ORCPT.
A *Recipient* object has the properties of Address, where address is
not null, and the following:
status: String See below.
De Gennaro Expires 1 May 2027 [Page 19]
Internet-Draft MTA Hooks October 2026
response: SmtpResponse|null The reply that determined the status:
the reply given to the RCPT command at inbound stages, or the
reply of the next hop at outbound stages.
decidedBy: String Label of the registration that last changed the
status, or "mta".
expansion: String[]|null Inbound only: the addresses this recipient
resolves to after alias expansion and rewriting, when the MTA has
already resolved it.
originalRecipient: String|null Outbound only: the address as given
in RCPT TO, when this recipient resulted from expansion or
rewriting.
attempts: UnsignedInt Outbound only: delivery attempts made for this
recipient.
nextAttemptAt: UTCDate|null Outbound only: when the next attempt is
scheduled.
nextDsnAt: UTCDate|null Outbound only: when the next delay DSN is
scheduled.
route: Route Outbound only: the route used for the next attempt
(Section 6.6). Its null properties take the MTA's defaults.
lastAttempt: Attempt|null Outbound only: the most recent delivery
attempt.
The status of a recipient is one of:
De Gennaro Expires 1 May 2027 [Page 20]
Internet-Draft MTA Hooks October 2026
+===========+====================================================+
| Status | Meaning |
+===========+====================================================+
| pending | The recipient of the RCPT command being processed. |
+-----------+----------------------------------------------------+
| accepted | Accepted at RCPT and not yet attempted. |
+-----------+----------------------------------------------------+
| deferred | Temporarily failed: a 4xx reply at RCPT, or an |
| | outbound attempt to be retried. |
+-----------+----------------------------------------------------+
| failed | Permanently failed: a 5xx reply at RCPT, or an |
| | outbound failure for which a DSN is due. |
+-----------+----------------------------------------------------+
| delivered | Delivered to the next hop. |
+-----------+----------------------------------------------------+
| discarded | Dropped without delivery and without a DSN. |
+-----------+----------------------------------------------------+
Table 2: Recipient Status
At inbound stages, recipients appear in the order of their RCPT
commands, including those rejected or deferred. At the "rcpt" stage
the last recipient is the one of the current command, with status
"pending". At outbound stages, the array contains the recipients
that the invocation covers: those of the delivery job matched by the
subscription, those of the transaction at the "transmit" stage, or
the recipient of the DSN at the "dsn" stage.
An *Attempt* object has the following properties:
at: UTCDate When the attempt was made.
remote: RemoteHost|null The host the MTA connected to, or null if no
connection was made.
sourceIp: String|null The local IP address used.
tls: TlsInfo|null TLS on the connection, or null if TLS was not
attempted.
A *RemoteHost* object has the following properties:
host: String Host name of the next hop, such as the MX name.
ip: String IP address connected to.
port: UnsignedInt Port connected to.
De Gennaro Expires 1 May 2027 [Page 21]
Internet-Draft MTA Hooks October 2026
5.5. Message
The "message" property is the Email object of Section 4 of [RFC8621],
without the metadata properties of Section 4.1.1 of [RFC8621] other
than "size", and with the "blobId" of each EmailBodyPart replaced by
a "blob" property of type Bytes holding the part's content after
decoding its Content-Transfer-Encoding [RFC2045]. The "rawMessage"
property is the message octet for octet, which signature verification
and archiving require.
A scanner can request either or both. Header changes are made
through "/message/headers"; the convenience properties derived from
headers, such as "subject" and "from", cannot be changed. Body
changes are made through "bodyValues" and "bodyStructure", or by
replacing "rawMessage" with a complete new message. A patch that
changes both "message" and "rawMessage" is invalid. After a change,
the MTA presents both consistently to the following scanners.
5.6. Sender Authentication
A *SenderAuth* object has the following properties, each holding a
result value of the corresponding Authentication-Results method
[RFC8601], or null if not evaluated:
iprev: String|null Reverse DNS check of the client address (forward-
confirmed reverse DNS).
spfHelo: String|null Sender Policy Framework (SPF) [RFC7208] for the
HELO identity.
spfMailFrom: String|null SPF for the MAIL FROM identity.
dkim: String|null DomainKeys Identified Mail (DKIM) [RFC6376].
dkim2: String|null DKIM2 [I-D.ietf-dkim-dkim2-spec], with the result
values of [I-D.gondwana-dkim2-authres].
dmarc: String|null Domain-based Message Authentication, Reporting,
and Conformance (DMARC) [RFC9989].
5.7. Queue
A *Queue* object has the following properties:
id: String Identifier of the message in the MTA's queue.
expiresAt: UTCDate|null When the MTA gives up delivering the
message. Present at outbound stages.
De Gennaro Expires 1 May 2027 [Page 22]
Internet-Draft MTA Hooks October 2026
The queue identifier MUST be the same at every stage and every
delivery job of the message, from "data" to the last "delivery" or
"dsn", and MUST be unique within the MTA for the lifetime of the
message. Scanners can therefore keep the state they collected at
inbound stages until expiresAt.
5.8. Extension Data
The "ext" object holds data outside this specification. Each key is
a namespace named by a reverse domain name controlled by its definer,
for example "org.example.spam", and its value is an object whose
content that definer specifies.
Extension data is written by the MTA and by scanners, and exchanged
like any other property: the subscription's properties decide which
namespaces a scanner receives, and its grants decide which it can
write. A namespace written by one scanner and sent to another is how
scanners pass results along a chain without adding header fields to
the message. Recipients of a request MUST ignore namespaces they do
not understand.
Extension data is kept with the message for as long as the message is
in the MTA, so data written at the "data" stage is available again at
the "route", "delivery", and "dsn" stages. Data written before the
MAIL command is kept with the session and copied to each of its
transactions. The MTA SHOULD limit the size of each namespace
through the "max" of the grant.
5.9. SMTP Response
An *SmtpResponse* object has the following properties:
code: UnsignedInt The reply code.
enhancedCode: String|null The enhanced status code [RFC3463].
message: String The reply text.
delayMs: UnsignedInt|null Milliseconds the MTA waits before sending
the reply. At the "connect" stage this delays the greeting, which
reveals clients that send commands before it. The MTA MUST cap
it, normally through the "max" of the grant, below the 5-minute
timeouts that SMTP clients apply (Section 4.5.3.2 of [RFC5321]).
De Gennaro Expires 1 May 2027 [Page 23]
Internet-Draft MTA Hooks October 2026
5.10. EHLO Capabilities
At the "ehlo" stage, a scanner can only withhold what the MTA offers:
it may remove keywords, remove space-separated parameters from a
value, or lower a numeric value such as that of SIZE. Other changes
to "capabilities" are invalid regardless of grants. This allows, for
example, offering AUTH only on some networks or withholding
PIPELINING and CHUNKING from suspicious clients.
5.11. Examples
An inbound request at the "data" stage, with the message abbreviated:
POST /v1/hooks/reg-7a3b9c2e HTTP/1.1
Host: scanner.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
MTA-Hooks-Registration: reg-7a3b9c2e
MTA-Hooks-Request-Id: 01JBDX3X7F4Y5N6Q9R2S3T4U5V
MTA-Hooks-Context: 01JBDX3TQ8V0D6H1E2F3G4H5J6
{
"stage": "data",
"action": "accept",
"decidedBy": "mta",
"response": {"code": 250, "enhancedCode": "2.0.0",
"message": "Queued as 4F2A31B0C9"},
"timestamp": "2026-10-28T16:30:00Z",
"timeoutMs": 4200,
"session": {
"id": "01JBDX3TQ8V0D6H1E2F3G4H5J6",
"startedAt": "2026-10-28T16:29:58Z",
"client": {"ip": "192.0.2.100", "port": 54321,
"ptr": "mail.example.org",
"ehlo": "mail.example.org", "asn": 64496},
"server": {"name": "mx1.example.com",
"ip": "198.51.100.25", "port": 25},
"tls": {"version": "TLSv1.3",
"cipher": "TLS_AES_256_GCM_SHA384",
"peerCertificate": null},
"auth": null,
"counters": {"commands": 5, "transactions": 1,
"pregreetBytes": 0}
},
"envelope": {
"from": {"address": "sender@example.org",
"parameters": {"SIZE": "2048", "BODY": "8BITMIME"}},
"to": [
De Gennaro Expires 1 May 2027 [Page 24]
Internet-Draft MTA Hooks October 2026
{"address": "bob@example.com", "parameters": {},
"status": "accepted", "decidedBy": "mta",
"response": {"code": 250, "message": "OK"}},
{"address": "nobody@example.com", "parameters": {},
"status": "failed", "decidedBy": "mta",
"response": {"code": 550, "enhancedCode": "5.1.1",
"message": "User unknown"}}
]
},
"message": {
"size": 2048,
"subject": "Meeting Tomorrow",
"headers": [
{"name": "From", "value": " Sender <sender@example.org>"},
{"name": "To", "value": " bob@example.com"},
{"name": "Subject", "value": " Meeting Tomorrow"}
],
"bodyValues": {
"1": {"value": "Let's meet tomorrow at 10am.",
"isEncodingProblem": false, "isTruncated": false}
}
},
"senderAuth": {"iprev": "pass", "spfMailFrom": "pass",
"dkim": "pass", "dkim2": "none", "dmarc": "pass"}
}
An outbound request at the "route" stage:
De Gennaro Expires 1 May 2027 [Page 25]
Internet-Draft MTA Hooks October 2026
{
"stage": "route",
"timestamp": "2026-10-28T17:00:00Z",
"timeoutMs": 2000,
"queue": {"id": "4F2A31B0C9", "expiresAt": "2026-11-02T16:30:00Z"},
"envelope": {
"from": {"address": "sales@example.com", "parameters": {}},
"to": [
{"address": "a@partner.example", "parameters": {},
"status": "deferred", "decidedBy": "mta",
"response": {"code": 451, "enhancedCode": "4.4.1",
"message": "Connection timed out"},
"attempts": 1, "nextAttemptAt": "2026-10-28T17:00:00Z",
"nextDsnAt": "2026-10-29T16:30:00Z",
"route": {"name": null},
"lastAttempt": {
"at": "2026-10-28T16:45:00Z",
"remote": {"host": "mx.partner.example",
"ip": "203.0.113.7", "port": 25},
"sourceIp": "198.51.100.25", "tls": null}}
]
},
"ext": {"org.example.spam": {"score": 0.4}}
}
6. Hook Response
6.1. Structure
A scanner answers with 200 and a JSON Patch [RFC6902] in the
negotiated serialization, or with 204 and no body when it has no
changes. An empty patch also means no changes. In JSON the Content-
Type is "application/json-patch+json"; in CBOR it is "application/
cbor" and the patch is the CBOR encoding of the same array.
The patch is applied to the hook request as held by the MTA, which
can include properties that were not sent. A scanner can, for
example, add a header field with "add" at "/message/headers/0"
without receiving the message, or append a recipient at "/envelope/
to/-". In that state, every property defined for an object that
exists at the stage is present, with the value null when it has none,
so "replace" and "test" behave the same whether or not a null
property was sent. The "/envelope/to" array is the one the request
carries, in the same order.
De Gennaro Expires 1 May 2027 [Page 26]
Internet-Draft MTA Hooks October 2026
6.2. Validation and Application
The MTA applies the patch to a copy of the state. The patch is valid
only if:
1. it applies successfully according to [RFC6902], including all
"test" operations;
2. the "from" of every "copy" and "move" operation, and the "path"
of every "test" operation, is covered by the subscription's
properties;
3. every location whose value differs between the original and the
resulting state is covered by a grant of the subscription, and
satisfies the "values" and "max" of the grant that applies
(Section 4.1.2);
4. it does not change "stage", "timestamp", "timeoutMs", any
"decidedBy", or a non-null "traceId", which no grant covers;
5. the resulting state respects the ordering of Section 6.3 unless
that grant relaxes it; and
6. the resulting state is otherwise valid: values have the types
defined in this document, actions and statuses are permitted at
the stage (Section 6.4, Section 6.5), at most one of "message"
and "rawMessage" changed, and header fields, addresses, and
message content are well formed (Section 9.5).
Because validation compares states, it does not depend on which
operations produced the result. A valid patch replaces the state,
after which the MTA sets "decidedBy" of the action and of each
changed recipient to the label of the registration. An invalid patch
is discarded as a whole, is logged with the request identifier, and
counts as a failure of the scanner (Section 7.3).
6.3. Ordering
Some properties are ordered from permissive to strict. Without
"relax", a scanner may only keep them, move them in the strict
direction, or move them between equally strict values:
De Gennaro Expires 1 May 2027 [Page 27]
Internet-Draft MTA Hooks October 2026
+=======================+===========================================+
| Property | Permissive to strict |
+=======================+===========================================+
| /action | accept, quarantine, then |
| | reject, discard, and disconnect |
| | (all equally strict) |
+-----------------------+-------------------------------------------+
| /envelope/to/*/route/ | none, opportunistic, required, |
| tls | verified |
+-----------------------+-------------------------------------------+
Table 3: Ordered Properties
A null "tls" ranks as the requirement the MTA would otherwise apply.
With "relax", the scanner may, for example, accept a message that the
MTA's own policy rejected or that an earlier scanner quarantined.
Such a grant makes the scanner the final authority for the property
(Section 9.2). When several scanners with "relax" change the same
property, the last one in the chain prevails.
6.4. Actions
At inbound stages, the action applies to the command being processed:
De Gennaro Expires 1 May 2027 [Page 28]
Internet-Draft MTA Hooks October 2026
+============+===============================================+
| Action | Meaning |
+============+===============================================+
| accept | Continue normally. |
+------------+-----------------------------------------------+
| quarantine | Accept the message and quarantine it instead |
| | of delivering it. Not permitted at the |
| | "connect" and "ehlo" stages. |
+------------+-----------------------------------------------+
| reject | Send "/response", a 4xx or 5xx reply. At the |
| | "connect" stage it replaces the greeting and |
| | MUST be a 554 reply (Section 3.1 of |
| | [RFC5321]); a temporary refusal of the |
| | connection is expressed with "disconnect". |
| | At the "rcpt" stage it rejects the recipient. |
+------------+-----------------------------------------------+
| discard | At the "mail" and "data" stages: accept the |
| | message and drop it. At the "rcpt" stage: |
| | accept the recipient and set it to |
| | "discarded". Not permitted at the "connect" |
| | and "ehlo" stages. |
+------------+-----------------------------------------------+
| disconnect | Send "/response", a 421 reply (Section 3.8 of |
| | [RFC5321]), and close the connection. |
+------------+-----------------------------------------------+
Table 4: Actions
The terminal actions are "reject", "discard", and "disconnect": after
a scanner sets one, the MTA invokes no further scanners for that
stage. At the "rcpt" stage, "reject" and "discard" apply only to the
current recipient, and processing of the transaction continues.
At the start of each inbound stage, the action is the one the MTA's
own policy assigns to the command, except that a "quarantine" set at
the "mail" or "rcpt" stage, or a "discard" set at the "mail" stage,
applies to the message and is kept for the later stages of the
transaction. The chain runs even if that initial action is terminal,
so that a scanner with "relax" can override it.
Outbound stages have no action: their outcome is the status of each
recipient (Section 6.5). The chain ends early when every recipient
of the invocation has been discarded.
De Gennaro Expires 1 May 2027 [Page 29]
Internet-Draft MTA Hooks October 2026
"accept", "quarantine", and "discard" require a 2xx reply, or the
intermediate reply SMTP requires for the command; "reject" requires a
4xx or 5xx reply; and "disconnect" requires a 421 reply. If
"/response" does not satisfy this, the MTA uses its default reply for
the action.
6.5. Recipient Changes
Recipients cannot be removed from "/envelope/to"; a recipient is
dropped by setting its status to "discarded". Recipients can be
added at the "data" stage, with status "accepted". The status of a
recipient can be changed as follows:
+===========================+===========+===================+
| Stage | From | To |
+===========================+===========+===================+
| data | accepted | discarded |
+---------------------------+-----------+-------------------+
| route, transmit, delivery | accepted, | deferred, failed, |
| | deferred | discarded |
+---------------------------+-----------+-------------------+
| delivery | failed | discarded |
+---------------------------+-----------+-------------------+
| dsn | accepted | discarded |
+---------------------------+-----------+-------------------+
Table 5: Recipient Status Changes
Setting a failed recipient to "discarded" suppresses its DSN, and
discarding the recipient of a DSN at the "dsn" stage suppresses that
DSN. A scanner that sets a recipient to "failed" or "deferred" MAY
also set its "response", which the MTA reports in the DSN; otherwise
the MTA uses a default reply. At the "route" and "delivery" stages,
"nextAttemptAt" and "nextDsnAt" can be changed for recipients that
remain deferred.
Discarding a message or a recipient after accepting it, and
suppressing a DSN, depart from Section 6.1 of [RFC5321] and are
subject to the cautions of Section 6.2 of [RFC5321]. MTAs SHOULD log
them, and operators SHOULD grant them only for unwanted mail or where
the sender is notified by other means.
At the "rcpt" stage the outcome of the current recipient is set
through "/action" and "/response", and the MTA records it in the
recipient's status. Per-recipient outcomes after the DATA command
are limited to "discarded" because SMTP gives a single reply to the
message.
De Gennaro Expires 1 May 2027 [Page 30]
Internet-Draft MTA Hooks October 2026
6.6. Routing
At the "route" stage, a scanner can choose how each recipient of the
job is delivered by changing its "route". A *Route* object has the
following properties:
name: String|null A route the MTA has configured, such as a queue or
transport name, carrying its own next hop, TLS, source address,
credential, and throttling settings. The names a scanner may use
are given by the "values" of its grant.
nextHop: String|null Host name or IP address to deliver to,
overriding the route's next hop.
port: UnsignedInt|null Port to connect to.
mxLookup: Boolean|null Whether "nextHop" is a domain whose MX
records are resolved (true) or a host to connect to directly
(false).
sourceIp: String|null Local IP address to connect from.
tls: String|null Minimum TLS requirement: "none", "opportunistic",
"required" (TLS without authentication of the server), or
"verified" (TLS with the server authenticated by DANE [RFC7672],
SMTP MTA Strict Transport Security (MTA-STS) [RFC8461], or PKIX
[RFC5280]).
A null property takes its value from the named route, or from the
MTA's default routing when "name" is null as well. Two routes are
different when any of their properties differ after null properties
have been resolved. Because each property can be granted separately,
an MTA can let a scanner choose only among predefined routes, or give
it complete control over next hops, source addresses, and TLS.
Credentials are never carried in a route; they belong to the named
route.
The MTA MUST NOT deliver recipients with different routes in the same
SMTP transaction. Otherwise, how recipients are batched remains
implementation-defined. A route change persists for the recipient
until changed again, and the "route" stage runs before every attempt,
so a scanner can fail over based on "attempts" and "lastAttempt".
The "delivery" stage runs once for the job, after all of its partial
deliveries.
A grant that relaxes "tls" lets the scanner lower the requirement
below the policy published by the destination, as a "TLS-Required:
No" header field does (Section 4.2.2 of [RFC8689]), for example to
De Gennaro Expires 1 May 2027 [Page 31]
Internet-Draft MTA Hooks October 2026
deliver a TLS report to a domain whose TLS is broken; lowering it to
"none" also skips STARTTLS. Like that header field, it does not
apply to a message sent with the REQUIRETLS parameter (Section 4.1 of
[RFC8689]). The MTA SHOULD log every route change together with the
registration label and the recipient, and MUST apply its loop
detection to scanner-chosen next hops.
6.7. Transmission
The "transmit" stage runs once for each SMTP transaction, after the
MTA has grouped the recipients of the job into transactions, so the
request carries the exact reverse-path and recipients that will be
sent. A scanner can change the message for this transaction only,
for example to add signatures that cover the envelope of each hop,
such as DKIM2 signatures [I-D.ietf-dkim-dkim2-spec]. Within the
envelope, only recipient status can be changed. Status changes are
kept; changes to the message apply to this transaction only and are
not kept in the queue. The stage runs again for every attempt, so
signatures carry the time and envelope of the attempt that uses them.
A DSN is changed the same way at the "dsn" stage.
6.8. Examples
Add header fields and pass a score to later scanners:
[
{"op": "add", "path": "/message/headers/0",
"value": {"name": "X-Spam-Status", "value": " No, score=0.4"}},
{"op": "add", "path": "/ext/org.example.spam",
"value": {"score": 0.4, "symbols": ["DKIM_VALID"]}}
]
Reject a message:
[
{"op": "replace", "path": "/action", "value": "reject"},
{"op": "replace", "path": "/response",
"value": {"code": 550, "enhancedCode": "5.7.1",
"message": "Message rejected: virus detected"}}
]
Slow down a suspicious client at the "connect" stage:
[
{"op": "add", "path": "/response/delayMs", "value": 15000}
]
De Gennaro Expires 1 May 2027 [Page 32]
Internet-Draft MTA Hooks October 2026
Route the first recipient through a virtual private network (VPN),
but only if no other scanner has already routed it:
[
{"op": "test", "path": "/envelope/to/0/route/name",
"value": null},
{"op": "replace", "path": "/envelope/to/0/route/name",
"value": "vpn-partner"}
]
Drop a failed recipient without a DSN at the "delivery" stage:
[
{"op": "replace", "path": "/envelope/to/2/status",
"value": "discarded"}
]
6.9. Error Responses
A scanner that cannot process a request returns a 4xx or 5xx status
with the error body of Appendix A:
* 401: Credentials missing or invalid. The MTA MUST NOT retry with
the same credentials.
* 403: Credentials not authorized for the registration. The MTA
MUST NOT retry.
* 404: Registration unknown. The MTA registers again.
* 413: Request body larger than the scanner accepts. The MTA MUST
NOT retry it.
* 422: The request uses unsupported properties or values. The MTA
MUST NOT retry it.
* 429: Rate limited. The MTA SHOULD NOT send further requests under
the registration until the time given by Retry-After, if any, and
applies onFailure to invocations meanwhile.
* 5xx: Transient error. The MTA MAY retry (Section 7.2).
Any other status is a failure that the MTA MUST NOT retry. MTAs
SHOULD alert their operators to 401, 403, 413, and 422, which persist
until the configuration changes; with "tempfail" they can hold mail
until the message expires.
De Gennaro Expires 1 May 2027 [Page 33]
Internet-Draft MTA Hooks October 2026
7. Transport
7.1. HTTP and TLS
Implementations MUST support HTTP/1.1 [RFC9112] and SHOULD support
HTTP/2 [RFC9113]; they MAY support HTTP/3 [RFC9114]. MTAs SHOULD
reuse connections to a scanner.
All MTA Hooks traffic MUST use TLS 1.2 or later and follow the
recommendations of [RFC9325]; implementations SHOULD support TLS 1.3
[RFC9846]. The MTA MUST validate the scanner's certificate; private
certificate authorities may be configured, but validation SHOULD NOT
be disabled.
7.2. Retries
On a network error or a 5xx status, the MTA MAY retry within the
request's timeoutMs, with exponential backoff and jitter.
7.3. Failure Handling
A scanner fails when it does not answer within timeoutMs, when all
retries fail, when it returns a status other than 200 or 204, or when
its patch is invalid (Section 6.2). The MTA then leaves the state
unchanged and acts according to the subscription's onFailure:
continue: Proceed with the next scanner, as if the scanner had made
no changes.
tempfail: At the "connect" and "ehlo" stages, answer with a 421
4.3.0 reply and close the connection; at the other inbound stages,
answer the current command with a 451 4.3.0 reply (Section 4.3.2
of [RFC5321]). At the "route" and "transmit" stages, leave the
invocation's accepted and deferred recipients deferred and retry
them later; a transaction skipped this way does not count as an
attempt. At the "dsn" stage, keep the DSN queued and retry it
later. At the "delivery" stage, outcomes are already final, so
the failure only means the scanner's changes are lost; a scanner
that must reliably suppress DSNs does so at the "dsn" stage.
"tempfail" is appropriate where a missing decision must not let mail
through, such as mandatory routing or mandatory archiving. It can
stop the affected mail flow while the scanner is unavailable, so
scanners used this way SHOULD be deployed redundantly, and
subscription filters SHOULD limit it to the mail that requires it.
MTAs SHOULD temporarily stop calling a scanner that fails repeatedly,
and apply its onFailure in the meantime.
De Gennaro Expires 1 May 2027 [Page 34]
Internet-Draft MTA Hooks October 2026
8. Implementation Status
Note to the RFC Editor: please remove this section, and the reference
to [RFC7942], before publication.
This section records the status of known implementations of the
protocol defined by this specification at the time of posting of this
Internet-Draft, and is based on a proposal described in [RFC7942].
The description of implementations in this section is intended to
assist the IETF in its decision processes in progressing drafts to
RFCs. Please note that the listing of any individual implementation
here does not imply endorsement by the IETF. Furthermore, no effort
has been spent to verify the information presented here that was
supplied by IETF contributors. This is not intended as, and must not
be construed to be, a catalog of available implementations or their
features. Readers are advised to note that other implementations may
exist.
Stalwart Mail Server: Organization: Stalwart Labs LLC, with which
the author is affiliated. Role: MTA. Coverage: an earlier
revision of this specification (-01). Licensing: AGPL-3.0 for the
open-source edition. Contact: mauro@stalw.art.
9. Security Considerations
9.1. Trust Model
The MTA acts only as an HTTP client and exposes no endpoint to
scanners. This removes attacks such as registration floods against
the MTA and request forgery through callbacks. The scanner operator
decides which MTAs may register and issues credentials; the MTA
operator decides which scanners to call and what each may change.
9.2. Authority of Scanners
A scanner can only change what its grants cover, and by default only
toward stricter processing. Grants that relax ordered properties,
change routes, or rewrite content make the scanner part of the
trusted base for those decisions: a compromised scanner with full
routing authority can redirect or exfiltrate mail, and one with
"relax" on "/action" can let malicious mail through. Operators
SHOULD give such grants only to scanners they trust accordingly,
limit them with subscription filters, and log their use. The MTA
MUST enforce grants itself and MUST NOT rely on scanners respecting
them.
De Gennaro Expires 1 May 2027 [Page 35]
Internet-Draft MTA Hooks October 2026
Scanner-chosen next hops and ports can also direct the MTA's
connections at internal services. An MTA SHOULD restrict them by
configuration, for example through the "values" of the grant on
"nextHop", and MUST apply to them the same address restrictions it
applies to next hops found in the DNS.
9.3. Credentials
Credentials SHOULD be issued per MTA instance, rotated, revoked when
an instance is decommissioned, and kept out of logs and diagnostics.
Bearer tokens MUST have at least 128 bits of entropy, and the
security considerations of [RFC6750] apply. Scanners SHOULD apply
authorization per MTA identity at registration and SHOULD rate limit
registrations per identity. The discovery document is
unauthenticated and SHOULD be rate limited.
9.4. Denial of Service
Slow or failing scanners can tie up MTA resources. MTAs MUST enforce
timeouts and SHOULD limit concurrent requests per scanner. Delayed
replies hold SMTP connections open; besides capping delayMs
(Section 5.9), the MTA SHOULD limit how many delayed replies it
serves at once. Recipients added at the "data" stage can amplify a
message, so MTAs SHOULD limit how many a patch may add. Scanners
SHOULD limit request rate, concurrency, and body size per
registration.
9.5. Injection
The MTA MUST validate everything a patch writes: header field names
and values MUST NOT allow injection of a carriage return (CR) or line
feed (LF), or malformed encoded words [RFC2047]; added or changed
addresses MUST be syntactically valid and subject to the MTA's relay
and recipient policy; and body changes MUST produce well-formed MIME
[RFC2045].
9.6. Unverified Data
Certificates that did not verify, proxy information, and client-
supplied values such as the EHLO argument are assertions by the peer.
Scanners MUST NOT treat them as authenticated identities. A trace
identifier received from outside the MTA's administrative domain can
be chosen by an attacker to link unrelated mail, so MTAs MUST NOT
accept one from untrusted sources.
De Gennaro Expires 1 May 2027 [Page 36]
Internet-Draft MTA Hooks October 2026
10. Privacy Considerations
Hook requests can contain message content, addresses, client IP
addresses, and authentication identities. MTAs SHOULD send only the
properties a scanner needs, and the subscription mechanism makes this
possible per stage and per part of the mail stream. Scanner
operators SHOULD avoid logging content and personal data, and SHOULD
limit retention to what their function requires.
11. IANA Considerations
11.1. Well-Known URI
IANA is requested to register the following well-known URI [RFC8615]:
* URI suffix: mta-hooks
* Change controller: IETF
* Specification document: this document
* Status: permanent
11.2. HTTP Field Names
IANA is requested to register the following field names in the
"Hypertext Transfer Protocol (HTTP) Field Name Registry"
(Section 16.3.1 of [RFC9110]), with status "permanent", no structured
type, and this document as the reference: MTA-Hooks-Registration,
MTA-Hooks-Request-Id, MTA-Hooks-Context.
11.3. MTA Hooks Registries
IANA is requested to create a new registry group named "MTA Hooks"
containing the four registries below. For all of them, new
registrations require Specification Required [RFC8126], and the
change controller of the initial entries is the IETF.
The designated experts SHOULD check that a name is a short identifier
in the style of the existing entries and does not duplicate the
semantics of an existing entry. For a stage, they SHOULD check that
the specification defines when it is invoked, what the request
contains, and which changes are permitted. For an action, they
SHOULD check that its meaning is defined at every stage where it is
permitted, and its place in the ordering of Section 6.3.
De Gennaro Expires 1 May 2027 [Page 37]
Internet-Draft MTA Hooks October 2026
11.3.1. MTA Hooks Stages
Each entry gives the name, the direction (inbound or outbound), when
the stage is invoked, the reference, and the change controller.
+==========+===========+======================+===========+
| Name | Direction | Invoked | Reference |
+==========+===========+======================+===========+
| connect | inbound | Client connected, | RFC XXXX |
| | | before the greeting | |
+----------+-----------+----------------------+-----------+
| ehlo | inbound | After each EHLO or | RFC XXXX |
| | | HELO command | |
+----------+-----------+----------------------+-----------+
| mail | inbound | After each MAIL | RFC XXXX |
| | | command | |
+----------+-----------+----------------------+-----------+
| rcpt | inbound | After each RCPT | RFC XXXX |
| | | command | |
+----------+-----------+----------------------+-----------+
| data | inbound | After the message | RFC XXXX |
| | | content | |
+----------+-----------+----------------------+-----------+
| route | outbound | Before each delivery | RFC XXXX |
| | | attempt of a job | |
+----------+-----------+----------------------+-----------+
| transmit | outbound | Before each SMTP | RFC XXXX |
| | | transaction | |
+----------+-----------+----------------------+-----------+
| delivery | outbound | After the attempts | RFC XXXX |
| | | of a job | |
+----------+-----------+----------------------+-----------+
| dsn | outbound | Before sending a | RFC XXXX |
| | | generated DSN | |
+----------+-----------+----------------------+-----------+
Table 6
11.3.2. MTA Hooks Actions
Each entry gives the name, whether the action is terminal, its rank
in the ordering of Section 6.3 (higher is stricter), the reference,
and the change controller. The meaning of each initial entry at each
stage is given in Table 4.
De Gennaro Expires 1 May 2027 [Page 38]
Internet-Draft MTA Hooks October 2026
+============+==========+======+===========+
| Name | Terminal | Rank | Reference |
+============+==========+======+===========+
| accept | no | 0 | RFC XXXX |
+------------+----------+------+-----------+
| quarantine | no | 1 | RFC XXXX |
+------------+----------+------+-----------+
| reject | yes | 2 | RFC XXXX |
+------------+----------+------+-----------+
| discard | yes | 2 | RFC XXXX |
+------------+----------+------+-----------+
| disconnect | yes | 2 | RFC XXXX |
+------------+----------+------+-----------+
Table 7
11.3.3. MTA Hooks Session Counters
Each entry gives the name, its meaning, the reference, and the change
controller. The reference of every initial entry is RFC XXXX.
De Gennaro Expires 1 May 2027 [Page 39]
Internet-Draft MTA Hooks October 2026
+======================+===================================+
| Name | Meaning |
+======================+===================================+
| commands | SMTP commands received |
+----------------------+-----------------------------------+
| errors | Commands answered with a 4xx or |
| | 5xx reply |
+----------------------+-----------------------------------+
| invalidCommands | Commands that were unknown or |
| | syntactically invalid |
+----------------------+-----------------------------------+
| rsets | RSET commands |
+----------------------+-----------------------------------+
| transactions | MAIL commands accepted, including |
| | the current transaction |
+----------------------+-----------------------------------+
| messagesAccepted | Messages accepted for delivery |
+----------------------+-----------------------------------+
| messagesRejected | Messages rejected after DATA |
+----------------------+-----------------------------------+
| pregreetBytes | Octets the client sent before the |
| | greeting |
+----------------------+-----------------------------------+
| pipeliningViolations | Commands sent ahead of a reply |
| | without PIPELINING, or across a |
| | synchronization point |
+----------------------+-----------------------------------+
| bareLineEnds | Lines ended by a bare LF or bare |
| | CR |
+----------------------+-----------------------------------+
| firstCommandDelayMs | Milliseconds between the greeting |
| | and the first command |
+----------------------+-----------------------------------+
Table 8: Initial Session Counters
11.3.4. MTA Hooks Error Codes
Each entry gives the code, the HTTP status it is used with, its
meaning, the reference, and the change controller. The initial
entries are those of Table 9, each with the reference RFC XXXX.
12. References
12.1. Normative References
De Gennaro Expires 1 May 2027 [Page 40]
Internet-Draft MTA Hooks October 2026
[ISO3166-1]
ISO, "Codes for the representation of names of countries
and their subdivisions -- Part 1: Country code",
ISO 3166-1:2020, 2020.
[RFC2045] Freed, N. and N. Borenstein, "Multipurpose Internet Mail
Extensions (MIME) Part One: Format of Internet Message
Bodies", RFC 2045, DOI 10.17487/RFC2045, November 1996,
<https://www.rfc-editor.org/rfc/rfc2045>.
[RFC2047] Moore, K., "MIME (Multipurpose Internet Mail Extensions)
Part Three: Message Header Extensions for Non-ASCII Text",
RFC 2047, DOI 10.17487/RFC2047, November 1996,
<https://www.rfc-editor.org/rfc/rfc2047>.
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119,
DOI 10.17487/RFC2119, March 1997,
<https://www.rfc-editor.org/rfc/rfc2119>.
[RFC3339] Klyne, G. and C. Newman, "Date and Time on the Internet:
Timestamps", RFC 3339, DOI 10.17487/RFC3339, July 2002,
<https://www.rfc-editor.org/rfc/rfc3339>.
[RFC3461] Moore, K., "Simple Mail Transfer Protocol (SMTP) Service
Extension for Delivery Status Notifications (DSNs)",
RFC 3461, DOI 10.17487/RFC3461, January 2003,
<https://www.rfc-editor.org/rfc/rfc3461>.
[RFC3463] Vaudreuil, G., "Enhanced Mail System Status Codes",
RFC 3463, DOI 10.17487/RFC3463, January 2003,
<https://www.rfc-editor.org/rfc/rfc3463>.
[RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
Resource Identifier (URI): Generic Syntax", STD 66,
RFC 3986, DOI 10.17487/RFC3986, January 2005,
<https://www.rfc-editor.org/rfc/rfc3986>.
[RFC4291] Hinden, R. and S. Deering, "IP Version 6 Addressing
Architecture", RFC 4291, DOI 10.17487/RFC4291, February
2006, <https://www.rfc-editor.org/rfc/rfc4291>.
[RFC4422] Melnikov, A., Ed. and K. Zeilenga, Ed., "Simple
Authentication and Security Layer (SASL)", RFC 4422,
DOI 10.17487/RFC4422, June 2006,
<https://www.rfc-editor.org/rfc/rfc4422>.
De Gennaro Expires 1 May 2027 [Page 41]
Internet-Draft MTA Hooks October 2026
[RFC4632] Fuller, V. and T. Li, "Classless Inter-domain Routing
(CIDR): The Internet Address Assignment and Aggregation
Plan", BCP 122, RFC 4632, DOI 10.17487/RFC4632, August
2006, <https://www.rfc-editor.org/rfc/rfc4632>.
[RFC4648] Josefsson, S., "The Base16, Base32, and Base64 Data
Encodings", RFC 4648, DOI 10.17487/RFC4648, October 2006,
<https://www.rfc-editor.org/rfc/rfc4648>.
[RFC4954] Siemborski, R., Ed. and A. Melnikov, Ed., "SMTP Service
Extension for Authentication", RFC 4954,
DOI 10.17487/RFC4954, July 2007,
<https://www.rfc-editor.org/rfc/rfc4954>.
[RFC5234] Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax
Specifications: ABNF", STD 68, RFC 5234,
DOI 10.17487/RFC5234, January 2008,
<https://www.rfc-editor.org/rfc/rfc5234>.
[RFC5280] Cooper, D., Santesson, S., Farrell, S., Boeyen, S.,
Housley, R., and W. Polk, "Internet X.509 Public Key
Infrastructure Certificate and Certificate Revocation List
(CRL) Profile", RFC 5280, DOI 10.17487/RFC5280, May 2008,
<https://www.rfc-editor.org/rfc/rfc5280>.
[RFC5321] Klensin, J., "Simple Mail Transfer Protocol", RFC 5321,
DOI 10.17487/RFC5321, October 2008,
<https://www.rfc-editor.org/rfc/rfc5321>.
[RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322,
DOI 10.17487/RFC5322, October 2008,
<https://www.rfc-editor.org/rfc/rfc5322>.
[RFC6750] Jones, M. and D. Hardt, "The OAuth 2.0 Authorization
Framework: Bearer Token Usage", RFC 6750,
DOI 10.17487/RFC6750, October 2012,
<https://www.rfc-editor.org/rfc/rfc6750>.
[RFC6901] Bryan, P., Ed., Zyp, K., and M. Nottingham, Ed.,
"JavaScript Object Notation (JSON) Pointer", RFC 6901,
DOI 10.17487/RFC6901, April 2013,
<https://www.rfc-editor.org/rfc/rfc6901>.
[RFC6902] Bryan, P., Ed. and M. Nottingham, Ed., "JavaScript Object
Notation (JSON) Patch", RFC 6902, DOI 10.17487/RFC6902,
April 2013, <https://www.rfc-editor.org/rfc/rfc6902>.
De Gennaro Expires 1 May 2027 [Page 42]
Internet-Draft MTA Hooks October 2026
[RFC7672] Dukhovni, V. and W. Hardaker, "SMTP Security via
Opportunistic DNS-Based Authentication of Named Entities
(DANE) Transport Layer Security (TLS)", RFC 7672,
DOI 10.17487/RFC7672, October 2015,
<https://www.rfc-editor.org/rfc/rfc7672>.
[RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for
Writing an IANA Considerations Section in RFCs", BCP 26,
RFC 8126, DOI 10.17487/RFC8126, June 2017,
<https://www.rfc-editor.org/rfc/rfc8126>.
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
May 2017, <https://www.rfc-editor.org/rfc/rfc8174>.
[RFC8259] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data
Interchange Format", STD 90, RFC 8259,
DOI 10.17487/RFC8259, December 2017,
<https://www.rfc-editor.org/rfc/rfc8259>.
[RFC8460] Margolis, D., Brotman, A., Ramakrishnan, B., Jones, J.,
and M. Risher, "SMTP TLS Reporting", RFC 8460,
DOI 10.17487/RFC8460, September 2018,
<https://www.rfc-editor.org/rfc/rfc8460>.
[RFC8461] Margolis, D., Risher, M., Ramakrishnan, B., Brotman, A.,
and J. Jones, "SMTP MTA Strict Transport Security (MTA-
STS)", RFC 8461, DOI 10.17487/RFC8461, September 2018,
<https://www.rfc-editor.org/rfc/rfc8461>.
[RFC8601] Kucherawy, M., "Message Header Field for Indicating
Message Authentication Status", RFC 8601,
DOI 10.17487/RFC8601, May 2019,
<https://www.rfc-editor.org/rfc/rfc8601>.
[RFC8615] Nottingham, M., "Well-Known Uniform Resource Identifiers
(URIs)", RFC 8615, DOI 10.17487/RFC8615, May 2019,
<https://www.rfc-editor.org/rfc/rfc8615>.
[RFC8620] Jenkins, N. and C. Newman, "The JSON Meta Application
Protocol (JMAP)", RFC 8620, DOI 10.17487/RFC8620, July
2019, <https://www.rfc-editor.org/rfc/rfc8620>.
[RFC8621] Jenkins, N. and C. Newman, "The JSON Meta Application
Protocol (JMAP) for Mail", RFC 8621, DOI 10.17487/RFC8621,
August 2019, <https://www.rfc-editor.org/rfc/rfc8621>.
De Gennaro Expires 1 May 2027 [Page 43]
Internet-Draft MTA Hooks October 2026
[RFC8689] Fenton, J., "SMTP Require TLS Option", RFC 8689,
DOI 10.17487/RFC8689, November 2019,
<https://www.rfc-editor.org/rfc/rfc8689>.
[RFC8949] Bormann, C. and P. Hoffman, "Concise Binary Object
Representation (CBOR)", STD 94, RFC 8949,
DOI 10.17487/RFC8949, December 2020,
<https://www.rfc-editor.org/rfc/rfc8949>.
[RFC9110] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke,
Ed., "HTTP Semantics", STD 97, RFC 9110,
DOI 10.17487/RFC9110, June 2022,
<https://www.rfc-editor.org/rfc/rfc9110>.
[RFC9112] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke,
Ed., "HTTP/1.1", STD 99, RFC 9112, DOI 10.17487/RFC9112,
June 2022, <https://www.rfc-editor.org/rfc/rfc9112>.
[RFC9113] Thomson, M., Ed. and C. Benfield, Ed., "HTTP/2", RFC 9113,
DOI 10.17487/RFC9113, June 2022,
<https://www.rfc-editor.org/rfc/rfc9113>.
[RFC9325] Sheffer, Y., Saint-Andre, P., and T. Fossati,
"Recommendations for Secure Use of Transport Layer
Security (TLS) and Datagram Transport Layer Security
(DTLS)", BCP 195, RFC 9325, DOI 10.17487/RFC9325, November
2022, <https://www.rfc-editor.org/rfc/rfc9325>.
[RFC9457] Nottingham, M., Wilde, E., and S. Dalal, "Problem Details
for HTTP APIs", RFC 9457, DOI 10.17487/RFC9457, July 2023,
<https://www.rfc-editor.org/rfc/rfc9457>.
[RFC9846] Rescorla, E., "The Transport Layer Security (TLS) Protocol
Version 1.3", RFC 9846, DOI 10.17487/RFC9846, July 2026,
<https://www.rfc-editor.org/rfc/rfc9846>.
[X.690] ITU-T, "Information technology - ASN.1 encoding rules:
Specification of Basic Encoding Rules (BER), Canonical
Encoding Rules (CER) and Distinguished Encoding Rules
(DER)", ITU-T Recommendation X.690, 2021.
12.2. Informative References
De Gennaro Expires 1 May 2027 [Page 44]
Internet-Draft MTA Hooks October 2026
[I-D.gondwana-dkim2-authres]
Gondwana, B., "Reporting DKIM2 Verification Results in
Authentication-Results", Work in Progress, Internet-Draft,
draft-gondwana-dkim2-authres-01, 28 October 2026,
<https://datatracker.ietf.org/doc/html/draft-gondwana-
dkim2-authres-01>.
[I-D.ietf-dkim-dkim2-spec]
Clayton, R., Chuang, W., and B. Gondwana, "DomainKeys
Identified Mail Signatures v2 (DKIM2)", Work in Progress,
Internet-Draft, draft-ietf-dkim-dkim2-spec-08, 27 October
2026, <https://datatracker.ietf.org/doc/html/draft-ietf-
dkim-dkim2-spec-08>.
[MILTER] Proofpoint, Inc., "Sendmail Mail Filter API (Milter)",
n.d., <https://www.sendmail.org/doc/sendmail-
current/libmilter/docs/>.
[PROXY] Tarreau, W., "The PROXY protocol, Versions 1 and 2", n.d.,
<https://www.haproxy.org/download/1.8/doc/proxy-
protocol.txt>.
[RFC6376] Crocker, D., Ed., Hansen, T., Ed., and M. Kucherawy, Ed.,
"DomainKeys Identified Mail (DKIM) Signatures", STD 76,
RFC 6376, DOI 10.17487/RFC6376, September 2011,
<https://www.rfc-editor.org/rfc/rfc6376>.
[RFC7208] Kitterman, S., "Sender Policy Framework (SPF) for
Authorizing Use of Domains in Email, Version 1", RFC 7208,
DOI 10.17487/RFC7208, April 2014,
<https://www.rfc-editor.org/rfc/rfc7208>.
[RFC7942] Sheffer, Y. and A. Farrel, "Improving Awareness of Running
Code: The Implementation Status Section", BCP 205,
RFC 7942, DOI 10.17487/RFC7942, July 2016,
<https://www.rfc-editor.org/rfc/rfc7942>.
[RFC8314] Moore, K. and C. Newman, "Cleartext Considered Obsolete:
Use of Transport Layer Security (TLS) for Email Submission
and Access", RFC 8314, DOI 10.17487/RFC8314, January 2018,
<https://www.rfc-editor.org/rfc/rfc8314>.
[RFC9114] Bishop, M., Ed., "HTTP/3", RFC 9114, DOI 10.17487/RFC9114,
June 2022, <https://www.rfc-editor.org/rfc/rfc9114>.
[RFC9562] Davis, K., Peabody, B., and P. Leach, "Universally Unique
IDentifiers (UUIDs)", RFC 9562, DOI 10.17487/RFC9562, May
2024, <https://www.rfc-editor.org/rfc/rfc9562>.
De Gennaro Expires 1 May 2027 [Page 45]
Internet-Draft MTA Hooks October 2026
[RFC9989] Herr, T., Ed. and J. Levine, Ed., "Domain-Based Message
Authentication, Reporting, and Conformance (DMARC)",
RFC 9989, DOI 10.17487/RFC9989, May 2026,
<https://www.rfc-editor.org/rfc/rfc9989>.
[XCLIENT] The Postfix Project, "Postfix XCLIENT Howto", n.d.,
<https://www.postfix.org/XCLIENT_README.html>.
[XFORWARD] The Postfix Project, "Postfix XFORWARD Howto", n.d.,
<https://www.postfix.org/XFORWARD_README.html>.
Appendix A. Error Codes
Error responses are Problem Details [RFC9457] with the media type
"application/problem+json", whatever serialization the registration
uses, and an extension member "code" holding one of the codes below:
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"code": "REGISTRATION_NOT_FOUND",
"detail": "No registration reg-7a3b9c2e"
}
+========+============================+===========================+
| Status | Code | Meaning |
+========+============================+===========================+
| 400 | INVALID_REQUEST | Malformed body or missing |
| | | required fields |
+--------+----------------------------+---------------------------+
| 400 | INVALID_FILTER | Filter is syntactically |
| | | invalid |
+--------+----------------------------+---------------------------+
| 400 | MISSING_REQUEST_ID | MTA-Hooks-Request-Id is |
| | | missing |
+--------+----------------------------+---------------------------+
| 401 | AUTHENTICATION_REQUIRED | No credentials were |
| | | provided |
+--------+----------------------------+---------------------------+
| 401 | INVALID_CREDENTIALS | Credentials are invalid |
| | | or expired |
+--------+----------------------------+---------------------------+
| 403 | REGISTRATION_DENIED | The MTA may not register |
+--------+----------------------------+---------------------------+
| 403 | REGISTRATION_MISMATCH | Credentials are not bound |
| | | to the registration |
+--------+----------------------------+---------------------------+
De Gennaro Expires 1 May 2027 [Page 46]
Internet-Draft MTA Hooks October 2026
| 404 | REGISTRATION_NOT_FOUND | No such registration |
+--------+----------------------------+---------------------------+
| 409 | REGISTRATION_LIMIT_REACHED | Too many registrations |
| | | for this MTA |
+--------+----------------------------+---------------------------+
| 413 | REQUEST_TOO_LARGE | Request body larger than |
| | | the scanner accepts |
+--------+----------------------------+---------------------------+
| 422 | UNSUPPORTED_SERIALIZATION | The serialization is not |
| | | supported |
+--------+----------------------------+---------------------------+
| 422 | UNSUPPORTED_STAGE | A stage is not supported |
+--------+----------------------------+---------------------------+
| 422 | UNSUPPORTED_PROPERTY | A property is not |
| | | supported |
+--------+----------------------------+---------------------------+
| 422 | UNSUPPORTED_FILTER | A filter condition is not |
| | | supported |
+--------+----------------------------+---------------------------+
| 429 | RATE_LIMITED | Too many requests |
+--------+----------------------------+---------------------------+
| 500 | INTERNAL_ERROR | Unexpected error |
+--------+----------------------------+---------------------------+
| 503 | UNAVAILABLE | Temporarily unable to |
| | | process requests |
+--------+----------------------------+---------------------------+
Table 9: Error Codes
Appendix B. Acknowledgments
The author thanks the operators of mailbox.org for their detailed
proposals on routing, observability, and inbound signals, and the
developers and operators of existing mail filtering systems whose
experience informed this design.
Appendix C. Changes
[[This section to be removed by RFC Editor]]
*draft-degennaro-mta-hooks-02*
* Responses are JSON Patch documents (RFC 6902), applied atomically;
the custom set/add/delete format and its ordering rules are
removed.
De Gennaro Expires 1 May 2027 [Page 47]
Internet-Draft MTA Hooks October 2026
* Registrations consist of ordered subscriptions, each with its own
filter, properties, grants, and failure policy. Filters gain
connection conditions.
* Replaced updateProperties with grants that can restrict values and
sizes. Ordered properties can only become stricter unless a grant
relaxes them, which replaces the downgrade rule. The discovery
document lists the changes a scanner wants as grants, replacing
its actions and updates lists.
* Actions apply to inbound stages only. Outbound outcomes,
including DSN suppression, are expressed through recipient status,
replacing "continue" and "cancel". Removed the "defer" stage,
which duplicated "delivery".
* At the "route" and "delivery" stages subscriptions are matched per
recipient, so grants and failure handling apply only to the
recipients a subscription matches.
* Added the "route" stage with per-recipient routes, and the
"transmit" stage, which runs per SMTP transaction so scanners can
sign each copy with its final envelope, as DKIM2 requires.
* Removed ARC, which the DMARC working group proposes to move to
Historic, from sender authentication and added DKIM2.
* Bearer tokens (RFC 6750) are mandatory to implement. Error
responses use Problem Details (RFC 9457). Header field values
have a defined syntax.
* Serializations are named by media type; removed the serialization
registry. The registration request carries the registration's
label, and no longer carries metadata. A scanner can only narrow
a registration by removing properties and grants.
* Certificates report a verification failure using RFC 8460 result
types. Sessions also describe non-SMTP submissions.
* One Recipient object across all stages with a common status set
and a decidedBy attribution; rejected recipients remain in the
array; "failed-silent" became "discarded".
* Properties are no longer split by direction. Client, TLS, and
authentication data are grouped into a Session object, which is
available at outbound stages when the MTA knows it.
De Gennaro Expires 1 May 2027 [Page 48]
Internet-Draft MTA Hooks October 2026
* Added session identifier and counters, proxy information, EHLO
capabilities, certificate chains with verification result, per-
attempt remote host and TLS details using RFC 8460 values,
delayMs, a per-request timeoutMs, traceId, and extension data with
message lifetime.
* Clarified ESMTP parameters and queue identifier stability. Added
iprev to sender authentication.
* Dropped the "X-" prefix from header fields (RFC 6648) and added
MTA-Hooks-Context.
* Simplified the registration lifecycle to GET and DELETE on the
registration URL; removed the status endpoint, its statistics, the
registration status and creation time, and the maxRegistrations
limit.
*draft-degennaro-mta-hooks-01*
* Inverted the registration model: the MTA is now exclusively the
HTTP client.
* Required a registration identifier and request identifier on every
hook request.
* Specified authentication, error responses, and action precedence.
*draft-degennaro-mta-hooks-00*
* Initial version
Author's Address
Mauro De Gennaro
Stalwart Labs LLC
1309 Coffeen Avenue, Suite 1200
Sheridan, WY 82801
United States of America
Email: mauro@stalw.art
URI: https://stalw.art
De Gennaro Expires 1 May 2027 [Page 49]