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