Internet-Draft MTA Hooks October 2026
De Gennaro Expires 1 May 2027 [Page]
Workgroup:
Network Working Group
Internet-Draft:
draft-degennaro-mta-hooks-02
Published:
Intended Status:
Standards Track
Expires:
Author:
M. De Gennaro
Stalwart Labs

Mail Transfer Agent (MTA) Hooks: An HTTP-Based Protocol for External Mail Processing

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.

▲

Table of Contents

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.

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.

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).

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

Table 1: 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.

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.

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
}

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".

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.

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.

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.

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.

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.

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.

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.

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.

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:

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.

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:

Table 2: Recipient Status
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:

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.

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.

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]).

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": [
      {"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}}
}

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.

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:

Table 3: Ordered Properties
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.

6.4. Actions

At inbound stages, the action applies to the command being processed:

Table 4: Actions
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.

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:

Table 5: Recipient Status Changes
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.

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 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}
]

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.

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.

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.

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.

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.

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.

Table 6
Name Direction Invoked Reference
connect inbound Client connected, before the greeting RFC XXXX
ehlo inbound After each EHLO or HELO command RFC XXXX
mail 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

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.

Table 7
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

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.

Table 8: Initial Session Counters
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

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

[ISO3166-1]
ISO, "Codes for the representation of names of countries and their subdivisions -- Part 1: Country code", ISO 3166-1: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, , <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, , <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, , <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, , <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, , <https://www.rfc-editor.org/rfc/rfc3461>.
[RFC3463]
Vaudreuil, G., "Enhanced Mail System Status Codes", RFC 3463, DOI 10.17487/RFC3463, , <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, , <https://www.rfc-editor.org/rfc/rfc3986>.
[RFC4291]
Hinden, R. and S. Deering, "IP Version 6 Addressing Architecture", RFC 4291, DOI 10.17487/RFC4291, , <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, , <https://www.rfc-editor.org/rfc/rfc4422>.
[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, , <https://www.rfc-editor.org/rfc/rfc4632>.
[RFC4648]
Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, , <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, , <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, , <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, , <https://www.rfc-editor.org/rfc/rfc5280>.
[RFC5321]
Klensin, J., "Simple Mail Transfer Protocol", RFC 5321, DOI 10.17487/RFC5321, , <https://www.rfc-editor.org/rfc/rfc5321>.
[RFC5322]
Resnick, P., Ed., "Internet Message Format", RFC 5322, DOI 10.17487/RFC5322, , <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, , <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, , <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, , <https://www.rfc-editor.org/rfc/rfc6902>.
[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, , <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, , <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, , <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, , <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, , <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, , <https://www.rfc-editor.org/rfc/rfc8461>.
[RFC8601]
Kucherawy, M., "Message Header Field for Indicating Message Authentication Status", RFC 8601, DOI 10.17487/RFC8601, , <https://www.rfc-editor.org/rfc/rfc8601>.
[RFC8615]
Nottingham, M., "Well-Known Uniform Resource Identifiers (URIs)", RFC 8615, DOI 10.17487/RFC8615, , <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, , <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, , <https://www.rfc-editor.org/rfc/rfc8621>.
[RFC8689]
Fenton, J., "SMTP Require TLS Option", RFC 8689, DOI 10.17487/RFC8689, , <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, , <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, , <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, , <https://www.rfc-editor.org/rfc/rfc9112>.
[RFC9113]
Thomson, M., Ed. and C. Benfield, Ed., "HTTP/2", RFC 9113, DOI 10.17487/RFC9113, , <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, , <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, , <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, , <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, .

12.2. Informative References

[I-D.gondwana-dkim2-authres]
Gondwana, B., "Reporting DKIM2 Verification Results in Authentication-Results", Work in Progress, Internet-Draft, draft-gondwana-dkim2-authres-01, , <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, , <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, , <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, , <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, , <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, , <https://www.rfc-editor.org/rfc/rfc8314>.
[RFC9114]
Bishop, M., Ed., "HTTP/3", RFC 9114, DOI 10.17487/RFC9114, , <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, , <https://www.rfc-editor.org/rfc/rfc9562>.
[RFC9989]
Herr, T., Ed. and J. Levine, Ed., "Domain-Based Message Authentication, Reporting, and Conformance (DMARC)", RFC 9989, DOI 10.17487/RFC9989, , <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"
}
Table 9: Error Codes
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

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

draft-degennaro-mta-hooks-01

draft-degennaro-mta-hooks-00

Author's Address

Mauro De Gennaro
Stalwart Labs LLC
1309 Coffeen Avenue, Suite 1200
Sheridan, WY 82801
United States of America