| Internet-Draft | MTA Hooks | October 2026 |
| De Gennaro | Expires 1 May 2027 | [Page] |
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.¶
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 (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. 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.¶
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.¶
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.¶
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.¶
An HTTP service that receives hook requests from an MTA and returns changes.¶
The state, held by the scanner, that records which stages, properties, and changes an MTA has agreed to exchange with it.¶
A part of a registration that applies to the messages and stages matching its filter.¶
A point in mail processing at which the MTA invokes scanners.¶
The object describing the current state that the MTA sends to a scanner.¶
A permission, given by the MTA, to change a part of the hook request.¶
A JSON Pointer [RFC6901] into the hook request, in which the reference token "*", used in place of an array index, matches any index.¶
A JSON string.¶
An integer in the range 0 <= value <= 2^53-1.¶
A JSON boolean.¶
A "date-time" string as defined in [RFC3339] whose time-offset is "Z".¶
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.¶
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 +---------+
Optionally, the MTA retrieves the scanner's discovery document (Section 3).¶
The MTA registers, stating the stages, properties, and grants for each subscription (Section 4).¶
At each subscribed stage the MTA sends a hook request (Section 5).¶
The scanner returns a JSON Patch, which the MTA validates against the grants and applies (Section 6).¶
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.¶
| 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. |
| 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. |
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).¶
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.¶
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.¶
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:¶
String
The protocol version, "1.0" for this specification. An MTA MUST NOT register with a scanner whose version it does not support.¶
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]).¶
String[]
Media types of the supported serializations for hook requests: "application/json", "application/cbor", or both.¶
String[]
Stages the scanner can process.¶
String[]
Property paths the scanner wants to receive.¶
Grant[]
Changes the scanner may want to make (Section 4.1.2).¶
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).¶
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
}
¶
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.¶
String
Human-readable name of the MTA instance.¶
String
The label of this registration in hook requests (Section 2.4).¶
String|null
MTA software version.¶
UnsignedInt
Longest time, in milliseconds, the MTA waits for a hook response.¶
UTCDate|null
Requested expiration. The scanner MAY assign a different one.¶
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.¶
Subscription[]
The subscriptions, in order. MUST NOT be empty.¶
A Subscription object has the following properties:¶
String[]
Stages this subscription applies to.¶
FilterOperator|FilterCondition|null
Which messages and connections this subscription applies to (Section 4.1.1). Null matches everything.¶
String[]
Property paths the MTA will send. A path includes everything below it.¶
Grant[]
Changes the scanner may make (Section 4.1.2).¶
String
What the MTA does when the scanner fails (Section 7.3): "continue" or "tempfail". Default "continue".¶
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.¶
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.¶
String
The MAIL FROM address matches.¶
String
The address of any recipient whose status is not "failed", "deferred", or "discarded" matches.¶
String
The client IP address is within the given address or Classless Inter-Domain Routing (CIDR) prefix ([RFC4632], Section 2.3 of [RFC4291]).¶
UnsignedInt
The client connected to this port.¶
Boolean
Whether the session uses TLS.¶
Boolean
Whether the client authenticated.¶
String
The authenticated login matches.¶
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.¶
A Grant object has the following properties:¶
String
A property path. The grant covers the locations it matches and everything below them.¶
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.¶
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.¶
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}
]
¶
On success the scanner returns 201 Created with a Location header field giving the registration URL, and a Registration object:¶
String
Registration identifier, with the syntax of Section 5.1.¶
UTCDate|null
Expiration time, or null for none.¶
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.¶
String
Confirmed serialization media type.¶
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.¶
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"
}
]
}
¶
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.¶
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.¶
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.¶
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:¶
REQUIRED. The registration identifier.¶
REQUIRED. Identifies the logical invocation. It MUST be the same on every retry of an invocation. Scanners SHOULD use it to deduplicate.¶
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¶
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.¶
String
The current stage.¶
String
Inbound stages only: what the MTA will do unless changed (Section 6.4).¶
String
Inbound stages only: label of the registration that last changed the action, or "mta".¶
SmtpResponse
Inbound stages only: the SMTP reply the MTA will send for the current command.¶
UTCDate
When the stage began.¶
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.¶
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|null
The session in which the message was received (Section 5.3).¶
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
Sender and recipients (Section 5.4).¶
Email
The parsed message (Section 5.5).¶
Bytes
SenderAuth
Sender authentication results (Section 5.6).¶
Queue
The queued message (Section 5.7).¶
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.¶
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.¶
String
Unique identifier of the session, stable across its stages and transactions.¶
UTCDate
When the connection was accepted.¶
ClientInfo|null
The client.¶
ServerInfo|null
The address the client connected to.¶
TlsInfo|null
TLS negotiated or attempted on the session, or null.¶
SmtpAuth|null
Authentication of the client, or null.¶
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:¶
String
Client IP address.¶
UnsignedInt
Client port.¶
String|null
Host name from the PTR record of the IP address.¶
String|null
Argument of the most recent EHLO or HELO command.¶
UnsignedInt|null
Autonomous System Number (ASN) of the IP address.¶
String|null
UnsignedInt|null
Concurrent connections from this IP address.¶
ProxyInfo|null
How the client identity was conveyed, if not taken from the socket.¶
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:¶
String
"proxy-v1" or "proxy-v2" for the PROXY protocol [PROXY], "xclient" for XCLIENT [XCLIENT], or "xforward" for XFORWARD [XFORWARD].¶
String
IP address of the proxy, the socket peer.¶
UnsignedInt
Port of the proxy.¶
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:¶
String
Host name the MTA uses on this connection.¶
String
IP address the client connected to.¶
UnsignedInt
Port the client connected to.¶
A TlsInfo object has the following properties:¶
String|null
TLS version: "TLSv1.2" or "TLSv1.3", or null if no handshake completed.¶
String|null
Name of the cipher suite as listed in the IANA "TLS Cipher Suites" registry, or null if no handshake completed.¶
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.¶
String|null
Outbound only: the policy type applied, as defined for "policy-type" in [RFC8460].¶
String|null
Outbound only: why TLS failed, as a "result-type" value of [RFC8460], or null.¶
A Certificate object has the following properties:¶
Bytes[]
The certificates as presented, encoded with the Distinguished Encoding Rules (DER) [X.690], end-entity certificate first.¶
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:¶
An Envelope object has the following properties:¶
An Address object has the following properties:¶
String|null
The address, or null for the null reverse-path.¶
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:¶
String
See below.¶
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.¶
String
Label of the registration that last changed the status, or "mta".¶
String[]|null
Inbound only: the addresses this recipient resolves to after alias expansion and rewriting, when the MTA has already resolved it.¶
String|null
Outbound only: the address as given in RCPT TO, when this recipient resulted from expansion or rewriting.¶
UnsignedInt
Outbound only: delivery attempts made for this recipient.¶
UTCDate|null
Outbound only: when the next attempt is scheduled.¶
UTCDate|null
Outbound only: when the next delay DSN is scheduled.¶
Route
Outbound only: the route used for the next attempt (Section 6.6). Its null properties take the MTA's defaults.¶
Attempt|null
Outbound only: the most recent delivery attempt.¶
The status of a recipient is one of:¶
| 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. |
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:¶
UTCDate
When the attempt was made.¶
RemoteHost|null
The host the MTA connected to, or null if no connection was made.¶
String|null
The local IP address used.¶
TlsInfo|null
TLS on the connection, or null if TLS was not attempted.¶
A RemoteHost object has the following properties:¶
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.¶
A SenderAuth object has the following properties, each holding a result value of the corresponding Authentication-Results method [RFC8601], or null if not evaluated:¶
String|null
Reverse DNS check of the client address (forward-confirmed reverse DNS).¶
String|null
Sender Policy Framework (SPF) [RFC7208] for the HELO identity.¶
String|null
SPF for the MAIL FROM identity.¶
String|null
String|null
DKIM2 [I-D.ietf-dkim-dkim2-spec], with the result values of [I-D.gondwana-dkim2-authres].¶
String|null
Domain-based Message Authentication, Reporting, and Conformance (DMARC) [RFC9989].¶
A Queue object has the following properties:¶
String
Identifier of the message in the MTA's queue.¶
UTCDate|null
When the MTA gives up delivering the message. Present at outbound stages.¶
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.¶
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.¶
An SmtpResponse object has the following properties:¶
UnsignedInt
The reply code.¶
String|null
String
The reply text.¶
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]).¶
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.¶
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": [
{"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:¶
{
"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}}
}
¶
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.¶
The MTA applies the patch to a copy of the state. The patch is valid only if:¶
it applies successfully according to [RFC6902], including all "test" operations;¶
the "from" of every "copy" and "move" operation, and the "path" of every "test" operation, is covered by the subscription's properties;¶
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);¶
it does not change "stage", "timestamp", "timeoutMs", any "decidedBy", or a non-null "traceId", which no grant covers;¶
the resulting state respects the ordering of Section 6.3 unless that grant relaxes it; and¶
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).¶
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:¶
| Property | Permissive to strict |
|---|---|
| /action | accept, quarantine, then reject, discard, and disconnect (all equally strict) |
| /envelope/to/*/route/tls | none, opportunistic, required, verified |
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.¶
At inbound stages, the action applies to the command being processed:¶
| 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. |
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.¶
"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.¶
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 | deferred, failed, discarded |
| delivery | failed | discarded |
| dsn | accepted | discarded |
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.¶
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:¶
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.¶
String|null
Host name or IP address to deliver to, overriding the route's next hop.¶
UnsignedInt|null
Port to connect to.¶
Boolean|null
Whether "nextHop" is a domain whose MX records are resolved (true) or a host to connect to directly (false).¶
String|null
Local IP address to connect from.¶
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 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.¶
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.¶
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}
]
¶
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"}
]
¶
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.¶
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.¶
On a network error or a 5xx status, the MTA MAY retry within the request's timeoutMs, with exponential backoff and jitter.¶
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:¶
Proceed with the next scanner, as if the scanner had made no changes.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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].¶
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.¶
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.¶
IANA is requested to register the following well-known URI [RFC8615]:¶
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.¶
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.¶
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, before the greeting | RFC XXXX |
| ehlo | inbound | After each EHLO or HELO command | RFC XXXX |
| inbound | After each MAIL command | RFC XXXX | |
| rcpt | inbound | After each RCPT command | RFC XXXX |
| data | inbound | After the message content | RFC XXXX |
| route | outbound | Before each delivery attempt of a job | RFC XXXX |
| transmit | outbound | Before each SMTP transaction | RFC XXXX |
| delivery | outbound | After the attempts of a job | RFC XXXX |
| dsn | outbound | Before sending a generated DSN | RFC XXXX |
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.¶
| 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 |
Each entry gives the name, its meaning, the reference, and the change controller. The reference of every initial entry is RFC XXXX.¶
| 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 |
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.¶
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 |
| 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 |
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.¶
[[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.¶
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.¶
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¶