Skip to main content

Mail Transfer Agent (MTA) Hooks: An HTTP-Based Protocol for External Mail Processing
draft-degennaro-mta-hooks-02

Document Type Active Internet-Draft (individual)
Author Mauro De Gennaro
Last updated 2026-10-28
RFC stream (None)
Intended RFC status (None)
Formats
Stream Stream state (No stream defined)
Consensus boilerplate Unknown
RFC Editor Note (None)
IESG IESG state I-D Exists
Telechat date (None)
Responsible AD (None)
Send notices to (None)
draft-degennaro-mta-hooks-02
Network Working Group                                      M. De Gennaro
Internet-Draft                                             Stalwart Labs
Intended status: Standards Track                         28 October 2026
Expires: 1 May 2027

  Mail Transfer Agent (MTA) Hooks: An HTTP-Based Protocol for External
                            Mail Processing
                      draft-degennaro-mta-hooks-02

Abstract

   This document specifies MTA Hooks, an HTTP-based protocol that lets
   Mail Transfer Agents (MTAs) delegate message processing decisions to
   external services.  The MTA sends a JSON or Concise Binary Object
   Representation (CBOR) description of the SMTP session, envelope, and
   message to a registered service at defined stages of reception and
   delivery.  The service answers with a JSON Patch that changes the
   description, and the MTA acts on the resulting state.  The same
   mechanism covers filtering at SMTP time, routing before delivery, and
   reacting to delivery outcomes, and the MTA controls exactly which
   changes each service may make.

Status of This Memo

   This Internet-Draft is submitted in full conformance with the
   provisions of BCP 78 and BCP 79.

   Internet-Drafts are working documents of the Internet Engineering
   Task Force (IETF).  Note that other groups may also distribute
   working documents as Internet-Drafts.  The list of current Internet-
   Drafts is at https://datatracker.ietf.org/drafts/current/.

   Internet-Drafts are draft documents valid for a maximum of six months
   and may be updated, replaced, or obsoleted by other documents at any
   time.  It is inappropriate to use Internet-Drafts as reference
   material or to cite them other than as "work in progress."

   This Internet-Draft will expire on 1 May 2027.

Copyright Notice

   Copyright (c) 2026 IETF Trust and the persons identified as the
   document authors.  All rights reserved.

   This document is subject to BCP 78 and the IETF Trust's Legal
   Provisions Relating to IETF Documents (https://trustee.ietf.org/
   license-info) in effect on the date of publication of this document.

De Gennaro                 Expires 1 May 2027                   [Page 1]
Internet-Draft                  MTA Hooks                   October 2026

   Please review these documents carefully, as they describe your rights
   and restrictions with respect to this document.  Code Components
   extracted from this document must include Revised BSD License text as
   described in Section 4.e of the Trust Legal Provisions and are
   provided without warranty as described in the Revised BSD License.

Table of Contents

   1.  Introduction  . . . . . . . . . . . . . . . . . . . . . . . .   3
     1.1.  Notational Conventions  . . . . . . . . . . . . . . . . .   4
     1.2.  Terminology . . . . . . . . . . . . . . . . . . . . . . .   4
     1.3.  Data Types  . . . . . . . . . . . . . . . . . . . . . . .   4
   2.  Protocol Overview . . . . . . . . . . . . . . . . . . . . . .   5
     2.1.  Architecture  . . . . . . . . . . . . . . . . . . . . . .   5
     2.2.  Serialization . . . . . . . . . . . . . . . . . . . . . .   6
     2.3.  Stages  . . . . . . . . . . . . . . . . . . . . . . . . .   6
     2.4.  Scanner Chains  . . . . . . . . . . . . . . . . . . . . .   7
     2.5.  State Changes . . . . . . . . . . . . . . . . . . . . . .   7
   3.  Discovery . . . . . . . . . . . . . . . . . . . . . . . . . .   7
   4.  Registration  . . . . . . . . . . . . . . . . . . . . . . . .   9
     4.1.  Registration Request  . . . . . . . . . . . . . . . . . .   9
       4.1.1.  Filters . . . . . . . . . . . . . . . . . . . . . . .  10
       4.1.2.  Grants  . . . . . . . . . . . . . . . . . . . . . . .  11
     4.2.  Registration Response . . . . . . . . . . . . . . . . . .  11
     4.3.  Authentication  . . . . . . . . . . . . . . . . . . . . .  14
     4.4.  Registration Lifecycle  . . . . . . . . . . . . . . . . .  14
   5.  Hook Request  . . . . . . . . . . . . . . . . . . . . . . . .  15
     5.1.  HTTP Request  . . . . . . . . . . . . . . . . . . . . . .  15
     5.2.  Properties  . . . . . . . . . . . . . . . . . . . . . . .  15
     5.3.  Session . . . . . . . . . . . . . . . . . . . . . . . . .  17
     5.4.  Envelope and Recipients . . . . . . . . . . . . . . . . .  19
     5.5.  Message . . . . . . . . . . . . . . . . . . . . . . . . .  22
     5.6.  Sender Authentication . . . . . . . . . . . . . . . . . .  22
     5.7.  Queue . . . . . . . . . . . . . . . . . . . . . . . . . .  22
     5.8.  Extension Data  . . . . . . . . . . . . . . . . . . . . .  23
     5.9.  SMTP Response . . . . . . . . . . . . . . . . . . . . . .  23
     5.10. EHLO Capabilities . . . . . . . . . . . . . . . . . . . .  24
     5.11. Examples  . . . . . . . . . . . . . . . . . . . . . . . .  24
   6.  Hook Response . . . . . . . . . . . . . . . . . . . . . . . .  26
     6.1.  Structure . . . . . . . . . . . . . . . . . . . . . . . .  26
     6.2.  Validation and Application  . . . . . . . . . . . . . . .  27
     6.3.  Ordering  . . . . . . . . . . . . . . . . . . . . . . . .  27
     6.4.  Actions . . . . . . . . . . . . . . . . . . . . . . . . .  28
     6.5.  Recipient Changes . . . . . . . . . . . . . . . . . . . .  30
     6.6.  Routing . . . . . . . . . . . . . . . . . . . . . . . . .  31
     6.7.  Transmission  . . . . . . . . . . . . . . . . . . . . . .  32
     6.8.  Examples  . . . . . . . . . . . . . . . . . . . . . . . .  32
     6.9.  Error Responses . . . . . . . . . . . . . . . . . . . . .  33

De Gennaro                 Expires 1 May 2027                   [Page 2]
Internet-Draft                  MTA Hooks                   October 2026

   7.  Transport . . . . . . . . . . . . . . . . . . . . . . . . . .  34
     7.1.  HTTP and TLS  . . . . . . . . . . . . . . . . . . . . . .  34
     7.2.  Retries . . . . . . . . . . . . . . . . . . . . . . . . .  34
     7.3.  Failure Handling  . . . . . . . . . . . . . . . . . . . .  34
   8.  Implementation Status . . . . . . . . . . . . . . . . . . . .  35
   9.  Security Considerations . . . . . . . . . . . . . . . . . . .  35
     9.1.  Trust Model . . . . . . . . . . . . . . . . . . . . . . .  35
     9.2.  Authority of Scanners . . . . . . . . . . . . . . . . . .  35
     9.3.  Credentials . . . . . . . . . . . . . . . . . . . . . . .  36
     9.4.  Denial of Service . . . . . . . . . . . . . . . . . . . .  36
     9.5.  Injection . . . . . . . . . . . . . . . . . . . . . . . .  36
     9.6.  Unverified Data . . . . . . . . . . . . . . . . . . . . .  36
   10. Privacy Considerations  . . . . . . . . . . . . . . . . . . .  37
   11. IANA Considerations . . . . . . . . . . . . . . . . . . . . .  37
     11.1.  Well-Known URI . . . . . . . . . . . . . . . . . . . . .  37
     11.2.  HTTP Field Names . . . . . . . . . . . . . . . . . . . .  37
     11.3.  MTA Hooks Registries . . . . . . . . . . . . . . . . . .  37
       11.3.1.  MTA Hooks Stages . . . . . . . . . . . . . . . . . .  38
       11.3.2.  MTA Hooks Actions  . . . . . . . . . . . . . . . . .  38
       11.3.3.  MTA Hooks Session Counters . . . . . . . . . . . . .  39
       11.3.4.  MTA Hooks Error Codes  . . . . . . . . . . . . . . .  40
   12. References  . . . . . . . . . . . . . . . . . . . . . . . . .  40
     12.1.  Normative References . . . . . . . . . . . . . . . . . .  40
     12.2.  Informative References . . . . . . . . . . . . . . . . .  44
   Appendix A.  Error Codes  . . . . . . . . . . . . . . . . . . . .  46
   Appendix B.  Acknowledgments  . . . . . . . . . . . . . . . . . .  47
   Appendix C.  Changes  . . . . . . . . . . . . . . . . . . . . . .  47
   Author's Address  . . . . . . . . . . . . . . . . . . . . . . . .  49

1.  Introduction

   Mail Transfer Agents need extensible mechanisms to integrate external
   services for spam filtering, virus scanning, policy enforcement,
   routing, and compliance.  Existing mechanisms each cover a part of
   the problem: content filter protocols such as Milter [MILTER] see
   only message reception, lookup tables receive a single key, and
   delivery outcomes are mostly recovered by parsing logs.

   MTA Hooks covers the whole life of a message with one protocol.  At
   each processing stage the MTA describes the current state as a single
   JavaScript Object Notation (JSON) [RFC8259] object: the SMTP session,
   the envelope with the status of every recipient, the message, and the
   delivery attempts.  A scanner answers with a JSON Patch [RFC6902]
   against that object.  Accepting, rejecting, adding a header,
   discarding a recipient, choosing a route, or suppressing a delivery
   status notification (DSN) [RFC3461] are all expressed the same way:
   as a change to the state.

De Gennaro                 Expires 1 May 2027                   [Page 3]
Internet-Draft                  MTA Hooks                   October 2026

   The MTA decides which parts of the object each scanner may change,
   and by default a scanner may only make processing stricter.  Making
   it more permissive, such as overriding a rejection or lowering a TLS
   requirement, has to be granted explicitly by the MTA operator.

   The protocol uses ordinary HTTP [RFC9110], so existing client and
   server libraries, load balancers, proxies, and monitoring systems
   work unchanged.

1.1.  Notational Conventions

   The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
   "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
   "OPTIONAL" in this document are to be interpreted as described in
   BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
   capitals, as shown here.

1.2.  Terminology

   MTA:  A Mail Transfer Agent as described in [RFC5321].

   Scanner:  An HTTP service that receives hook requests from an MTA and
      returns changes.

   Registration:  The state, held by the scanner, that records which
      stages, properties, and changes an MTA has agreed to exchange with
      it.

   Subscription:  A part of a registration that applies to the messages
      and stages matching its filter.

   Stage:  A point in mail processing at which the MTA invokes scanners.

   Hook request:  The object describing the current state that the MTA
      sends to a scanner.

   Grant:  A permission, given by the MTA, to change a part of the hook
      request.

   Property path:  A JSON Pointer [RFC6901] into the hook request, in
      which the reference token "*", used in place of an array index,
      matches any index.

1.3.  Data Types

   String:  A JSON string.

   UnsignedInt:  An integer in the range 0 <= value <= 2^53-1.

De Gennaro                 Expires 1 May 2027                   [Page 4]
Internet-Draft                  MTA Hooks                   October 2026

   Boolean:  A JSON boolean.

   UTCDate:  A "date-time" string as defined in [RFC3339] whose time-
      offset is "Z".

   Bytes:  Binary data.  In JSON it is a base64 string (Section 4 of
      [RFC4648]); in CBOR it is a byte string.

   A[] denotes an array of A, and String[A] denotes an object whose keys
   are strings and whose values are of type A.  When sent, a property
   whose type includes null MAY be omitted, which is equivalent to null,
   and a property with a stated default MAY be omitted, which is
   equivalent to the default.

2.  Protocol Overview

2.1.  Architecture

   The MTA is always the HTTP client.  It discovers a scanner, registers
   with it, and then sends it hook requests during mail processing.  The
   MTA exposes no HTTP endpoint.

   +-----+   GET /.well-known/mta-hooks          +---------+
   |     |-------------------------------------->|         |
   |     |   POST registration                   |         |
   | MTA |-------------------------------------->| Scanner |
   |     |   POST hook request                   |         |
   |     |-------------------------------------->|         |
   |     |<--------------------------------------|         |
   +-----+   JSON Patch                          +---------+

                     Figure 1: MTA Hooks Protocol Flow

   1.  Optionally, the MTA retrieves the scanner's discovery document
       (Section 3).

   2.  The MTA registers, stating the stages, properties, and grants for
       each subscription (Section 4).

   3.  At each subscribed stage the MTA sends a hook request
       (Section 5).

   4.  The scanner returns a JSON Patch, which the MTA validates against
       the grants and applies (Section 6).

De Gennaro                 Expires 1 May 2027                   [Page 5]
Internet-Draft                  MTA Hooks                   October 2026

2.2.  Serialization

   Discovery, registration, and error responses use JSON.  Hook requests
   and responses use JSON or CBOR [RFC8949], chosen at registration.  In
   CBOR, the data model is that of JSON, with text strings as map keys,
   except that Bytes values are byte strings, which avoids the size
   increase of base64 for message content.  JSON Pointer and JSON Patch
   apply unchanged, and a "test" operation compares byte strings octet
   for octet.

2.3.  Stages

         +==========+============================================+
         | Stage    | Invoked                                    |
         +==========+============================================+
         | connect  | When a client connects, before the         |
         |          | greeting is sent; on a port with implicit  |
         |          | TLS [RFC8314], after the TLS handshake.    |
         +----------+--------------------------------------------+
         | ehlo     | After each EHLO or HELO command, including |
         |          | the one that follows STARTTLS.             |
         +----------+--------------------------------------------+
         | mail     | After each MAIL command.                   |
         +----------+--------------------------------------------+
         | rcpt     | After each RCPT command.                   |
         +----------+--------------------------------------------+
         | data     | After the message content has been         |
         |          | received, before the final reply.          |
         +----------+--------------------------------------------+
         | route    | Each time the MTA takes recipients from    |
         |          | the queue for a delivery attempt, before   |
         |          | resolving or connecting to the next hop.   |
         +----------+--------------------------------------------+
         | transmit | Before each SMTP transaction of a delivery |
         |          | attempt, once its envelope is final and    |
         |          | before the message is transmitted.         |
         +----------+--------------------------------------------+
         | delivery | After the delivery attempts of a delivery  |
         |          | job have completed.                        |
         +----------+--------------------------------------------+
         | dsn      | Before the MTA sends a delivery status     |
         |          | notification it generated.                 |
         +----------+--------------------------------------------+

                              Table 1: Stages

De Gennaro                 Expires 1 May 2027                   [Page 6]
Internet-Draft                  MTA Hooks                   October 2026

   The first five stages are inbound stages and the last four are
   outbound stages.  Each inbound stage request carries the state
   accumulated so far in the session, subject to the subscription's
   properties, so a scanner that subscribes only to "data" receives the
   connection, TLS, authentication, envelope, and message in a single
   request and can still reject the message at SMTP time.

   A delivery job is a set of recipients of one message that the MTA
   attempts in one processing cycle.  How recipients are grouped into
   jobs is implementation-defined, except as constrained by routing
   (Section 6.6).

2.4.  Scanner Chains

   When several registrations subscribe to the same stage, the MTA
   invokes them sequentially in a configured order.  Each scanner
   receives the state as changed by the previous scanners, including the
   current action and which scanner set it.  The chain ends early when a
   scanner sets a terminal action (Section 6.4).

   Each registration is identified in hook requests by a label that the
   MTA operator assigns to it, for example "spam" or "archive", and that
   the MTA sends in the registration request.  The label is not chosen
   by the scanner, is stable across re-registration, and is unique
   within the MTA.  It has the syntax of the registration identifier
   (Section 5.1), with at most 64 characters.  The reserved label "mta"
   denotes the MTA itself.

2.5.  State Changes

   A scanner changes processing by returning a JSON Patch [RFC6902] that
   is applied to the hook request.  The MTA then acts on the resulting
   state.  There is no other way to influence processing, so every
   effect a scanner can have corresponds to a path in the hook request,
   and every permission is a grant on a path.

3.  Discovery

   A scanner MAY publish a discovery document at "/.well-known/mta-
   hooks".  The MTA retrieves it with GET.  Discovery is optional: the
   MTA may instead be configured with the registration URL.

   The discovery document has the following properties:

   version: String  The protocol version, "1.0" for this specification.
      An MTA MUST NOT register with a scanner whose version it does not
      support.

De Gennaro                 Expires 1 May 2027                   [Page 7]
Internet-Draft                  MTA Hooks                   October 2026

   registration: String  URL of the registration endpoint, an absolute
      URL or a relative reference resolved against the URL of the
      discovery document (Section 5 of [RFC3986]).

   serialization: String[]  Media types of the supported serializations
      for hook requests: "application/json", "application/cbor", or
      both.

   stages: String[]  Stages the scanner can process.

   properties: String[]  Property paths the scanner wants to receive.

   grants: Grant[]  Changes the scanner may want to make
      (Section 4.1.2).

   maxMessageSize: UnsignedInt|null  Largest message, in octets, the
      scanner accepts.  The MTA does not send a request for a larger
      message and instead treats the invocation as a failure of the
      scanner (Section 7.3).

   timeoutMs: UnsignedInt|null  Time, in milliseconds, the scanner needs
      to answer a typical request.

   {
     "version": "1.0",
     "registration": "/v1/registrations",
     "serialization": ["application/cbor", "application/json"],
     "stages": ["connect", "rcpt", "data", "route", "delivery"],
     "properties": ["/session", "/envelope", "/message",
                    "/senderAuth", "/queue", "/ext/org.example.av"],
     "grants": [
       {"path": "/action",
        "values": ["accept", "quarantine", "reject", "discard"]},
       {"path": "/response"},
       {"path": "/message/headers"},
       {"path": "/envelope/to/*/status"},
       {"path": "/envelope/to/*/route"},
       {"path": "/ext/org.example.spam"}
     ],
     "maxMessageSize": 52428800,
     "timeoutMs": 2000
   }

De Gennaro                 Expires 1 May 2027                   [Page 8]
Internet-Draft                  MTA Hooks                   October 2026

4.  Registration

   The MTA registers by POSTing a registration request to the
   registration endpoint.  The MTA builds the request from its
   configuration; it states what the MTA will send and what it will
   allow.  The scanner can accept it, narrow it, or refuse it, but never
   widen it.

4.1.  Registration Request

   name: String  Human-readable name of the MTA instance.

   label: String  The label of this registration in hook requests
      (Section 2.4).

   softwareVersion: String|null  MTA software version.

   timeoutMs: UnsignedInt  Longest time, in milliseconds, the MTA waits
      for a hook response.

   expiresAt: UTCDate|null  Requested expiration.  The scanner MAY
      assign a different one.

   serialization: String  Media type of the serialization for hook
      requests and responses.  A scanner that does not support it
      refuses the registration with 422 UNSUPPORTED_SERIALIZATION.

   subscriptions: Subscription[]  The subscriptions, in order.  MUST NOT
      be empty.

   A *Subscription* object has the following properties:

   stages: String[]  Stages this subscription applies to.

   filter: FilterOperator|FilterCondition|null  Which messages and
      connections this subscription applies to (Section 4.1.1).  Null
      matches everything.

   properties: String[]  Property paths the MTA will send.  A path
      includes everything below it.

   grants: Grant[]  Changes the scanner may make (Section 4.1.2).

   onFailure: String  What the MTA does when the scanner fails
      (Section 7.3): "continue" or "tempfail".  Default "continue".

De Gennaro                 Expires 1 May 2027                   [Page 9]
Internet-Draft                  MTA Hooks                   October 2026

   For each stage, the MTA uses the first subscription, in order, that
   lists the stage and whose filter matches.  If none matches, the
   scanner is not invoked.  Different parts of the mail stream can
   therefore receive different properties, permissions, and failure
   handling.

   At the "route" and "delivery" stages, subscriptions are matched per
   recipient, with envelopeTo evaluated against that recipient alone.
   When recipients of a job match different subscriptions of the same
   registration, the MTA invokes the scanner once for each of those
   subscriptions, in subscription order, with only the recipients it
   matches, so each subscription's grants and onFailure apply only to
   its own recipients.  At the "transmit" and "dsn" stages the
   transaction or DSN is matched as a whole, because its changes apply
   to a single copy.

4.1.1.  Filters

   A filter uses the FilterOperator and FilterCondition syntax of
   Section 5.5 of [RFC8620] with the Email filter conditions of
   Section 4.4.1 of [RFC8621], except those that depend on mailbox or
   thread state: inMailbox, inMailboxOtherThan, before, after,
   hasKeyword, notKeyword, allInThreadHaveKeyword,
   someInThreadHaveKeyword, and noneInThreadHaveKeyword.

   The following conditions are added.  Their String values MAY contain
   "*", which matches zero or more characters.  Domains are compared
   case-insensitively; local parts and logins are compared exactly.

   envelopeFrom: String  The MAIL FROM address matches.

   envelopeTo: String  The address of any recipient whose status is not
      "failed", "deferred", or "discarded" matches.

   clientIp: String  The client IP address is within the given address
      or Classless Inter-Domain Routing (CIDR) prefix ([RFC4632],
      Section 2.3 of [RFC4291]).

   serverPort: UnsignedInt  The client connected to this port.

   tls: Boolean  Whether the session uses TLS.

   authenticated: Boolean  Whether the client authenticated.

   authLogin: String  The authenticated login matches.

De Gennaro                 Expires 1 May 2027                  [Page 10]
Internet-Draft                  MTA Hooks                   October 2026

   A condition that refers to information not available at the stage
   being evaluated, such as the subject at the "rcpt" stage or the
   client address of a locally submitted message, evaluates to false, so
   its negation evaluates to true.

   The filter is evaluated by the MTA.  It is included in the
   registration so the scanner knows which traffic it will see.

4.1.2.  Grants

   A *Grant* object has the following properties:

   path: String  A property path.  The grant covers the locations it
      matches and everything below them.

   values: String[]|null  If not null, every String value at or below
      the path that differs after the patch MUST match one of these
      values, which MAY contain the "*" wildcard of Section 4.1.1.

   max: UnsignedInt|null  If not null, a number at the path MUST NOT
      exceed it after the patch; any other value at the path MUST NOT
      exceed it in size, in octets, in the negotiated serialization.

   relax: Boolean  Whether the scanner may change ordered properties in
      the permissive direction (Section 6.3).  Default false.

   A change to a location not covered by a grant is not permitted.  When
   several grants cover a location, the most specific one applies: the
   one whose path has the most reference tokens and, among those, the
   one with a literal token where the others have "*".  A narrower grant
   can thus restrict part of a broader one.  Examples:

   [
     {"path": "/message/headers"},
     {"path": "/action", "relax": true},
     {"path": "/envelope/to/*/route/name",
      "values": ["default", "vpn-*"]},
     {"path": "/response/delayMs", "max": 30000},
     {"path": "/ext/org.example.spam", "max": 4096}
   ]

4.2.  Registration Response

   On success the scanner returns 201 Created with a Location header
   field giving the registration URL, and a Registration object:

   id: String  Registration identifier, with the syntax of Section 5.1.

De Gennaro                 Expires 1 May 2027                  [Page 11]
Internet-Draft                  MTA Hooks                   October 2026

   expiresAt: UTCDate|null  Expiration time, or null for none.

   hookEndpoint: String  URL to which hook requests are sent, an
      absolute URL or a relative reference resolved against the
      registration URL.

   An absolute URL in "registration" or "hookEndpoint" MUST have the
   same origin as the URL it is resolved against, unless the MTA's
   configuration allows the other origin.  The MTA MUST NOT follow
   redirects for registration and hook requests; a 3xx status is a
   failure.

   serialization: String  Confirmed serialization media type.

   subscriptions: Subscription[]  The subscriptions as accepted.  The
      scanner MAY remove properties and grants it does not need, but
      MUST NOT otherwise change the subscriptions, because removing a
      stage or a subscription would let traffic fall through to a later
      subscription.  A scanner that cannot serve a subscription refuses
      the registration with 422.  The MTA MUST NOT send properties or
      honor changes beyond what is confirmed here.

De Gennaro                 Expires 1 May 2027                  [Page 12]
Internet-Draft                  MTA Hooks                   October 2026

   POST /v1/registrations HTTP/1.1
   Host: scanner.example.com
   Content-Type: application/json
   Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

   {
     "name": "mx1.example.com",
     "label": "spam",
     "softwareVersion": "2.1.0",
     "timeoutMs": 5000,
     "serialization": "application/json",
     "subscriptions": [
       {
         "stages": ["data"],
         "filter": {"authenticated": false},
         "properties": ["/session", "/envelope", "/message",
                        "/senderAuth"],
         "grants": [
           {"path": "/action"},
           {"path": "/response"},
           {"path": "/message/headers"},
           {"path": "/ext/org.example.spam", "max": 4096}
         ],
         "onFailure": "continue"
       },
       {
         "stages": ["route"],
         "filter": {"envelopeTo": "*@partner.example"},
         "properties": ["/envelope", "/queue",
                        "/ext/org.example.spam"],
         "grants": [
           {"path": "/envelope/to/*/route/name",
            "values": ["vpn-partner"]}
         ],
         "onFailure": "tempfail"
       }
     ]
   }

De Gennaro                 Expires 1 May 2027                  [Page 13]
Internet-Draft                  MTA Hooks                   October 2026

   HTTP/1.1 201 Created
   Content-Type: application/json
   Location: /v1/registrations/reg-7a3b9c2e

   {
     "id": "reg-7a3b9c2e",
     "expiresAt": "2027-10-28T10:00:00Z",
     "hookEndpoint": "/v1/hooks/reg-7a3b9c2e",
     "serialization": "application/json",
     "subscriptions": [ ... ]
   }

   In this response, which echoes the two requested subscriptions
   unchanged, they are abbreviated.  The HTTP examples in this document
   omit Content-Length.

4.3.  Authentication

   The MTA MUST authenticate every request it makes to the scanner other
   than retrieval of the discovery document.  Implementations MUST
   support bearer tokens [RFC6750] in the Authorization header field,
   and MAY support TLS client certificates instead.  Credentials are
   issued by the scanner operator to each MTA through an out-of-band
   channel.  The scanner MUST reject unauthenticated requests with 401,
   including a WWW-Authenticate header field as [RFC6750] requires, and
   requests from an MTA not authorized for the registration with 403.

4.4.  Registration Lifecycle

   The MTA MAY retrieve the current Registration object with GET on the
   registration URL.  It deregisters with DELETE on that URL, to which
   the scanner answers 204 (No Content).  After deregistration the MTA
   MUST NOT start new hook requests for the registration.  Hook requests
   already in flight MAY complete.

   A registration that expires or is terminated by the scanner causes
   subsequent requests to fail with 404 REGISTRATION_NOT_FOUND.  The MTA
   MUST treat that response on a previously active registration as a
   signal to register again.  The MTA SHOULD register again before
   expiresAt, and when its configuration for the scanner changes.  Each
   registration has a new identifier; once the new registration is
   active, the MTA SHOULD delete the one it replaces.  A scanner SHOULD
   continue to accept hook requests for a replaced registration for a
   short grace period.

   Scanner instances behind a load balancer MUST share registration
   state or partition it by registration identifier.

De Gennaro                 Expires 1 May 2027                  [Page 14]
Internet-Draft                  MTA Hooks                   October 2026

5.  Hook Request

5.1.  HTTP Request

   The MTA sends each hook request with POST to the hookEndpoint, with a
   Content-Type of "application/json" or "application/cbor" and the
   following header fields:

   MTA-Hooks-Registration:  REQUIRED.  The registration identifier.

   MTA-Hooks-Request-Id:  REQUIRED.  Identifies the logical invocation.
      It MUST be the same on every retry of an invocation.  Scanners
      SHOULD use it to deduplicate.

   MTA-Hooks-Context:  OPTIONAL.  An opaque value that is the same for
      all requests about the same session at inbound stages, and about
      the same message at outbound stages, so that load balancers can
      route related requests to the same scanner instance without
      parsing the body.  Scanners MUST NOT depend on such affinity.

   Their values have the following syntax, using ABNF [RFC5234] and the
   "unreserved" rule of Section 2.3 of [RFC3986]:

   MTA-Hooks-Registration = 1*255( ALPHA / DIGIT / "-" / "_" / ":" )
   MTA-Hooks-Request-Id   = 1*128unreserved
   MTA-Hooks-Context      = 1*128unreserved

5.2.  Properties

   The hook request is an object whose properties are listed below.
   "stage", "timestamp", and "timeoutMs" are always sent, and "action",
   "decidedBy", and "response" are always sent at inbound stages.  The
   others are sent when they are covered by the subscription's
   properties and available at the stage.

   stage: String  The current stage.

   action: String  Inbound stages only: what the MTA will do unless
      changed (Section 6.4).

   decidedBy: String  Inbound stages only: label of the registration
      that last changed the action, or "mta".

   response: SmtpResponse  Inbound stages only: the SMTP reply the MTA
      will send for the current command.

   timestamp: UTCDate  When the stage began.

De Gennaro                 Expires 1 May 2027                  [Page 15]
Internet-Draft                  MTA Hooks                   October 2026

   timeoutMs: UnsignedInt  Milliseconds the scanner has to answer this
      request: the registration's timeoutMs, reduced by any budget the
      MTA's configuration assigns to the stage or the chain and by time
      already spent on earlier attempts of this invocation.  Answers
      arriving later are discarded.

   traceId: String|null  Opaque identifier that correlates the message
      across the MTAs of an administrative domain.  How it is obtained
      and propagated between MTAs is outside the scope of this document.
      It SHOULD be globally unique, for example a version 7 Universally
      Unique Identifier (UUIDv7) [RFC9562].  A scanner with a grant MAY
      set it while it is null; once set, it cannot be changed.

   session: Session|null  The session in which the message was received
      (Section 5.3).

   capabilities: String[String|null]  At the "ehlo" stage, the SMTP
      service extensions the MTA will announce, keyed by upper-case
      keyword, with the parameters as a space-separated string or null
      (Section 5.10).

   envelope: Envelope  Sender and recipients (Section 5.4).

   message: Email  The parsed message (Section 5.5).

   rawMessage: Bytes  The message in Internet Message Format [RFC5322].

   senderAuth: SenderAuth  Sender authentication results (Section 5.6).

   queue: Queue  The queued message (Section 5.7).

   ext: String[Object]  Extension data (Section 5.8).

   A property is not sent when the MTA does not have its information,
   such as the envelope before MAIL, the message before the end of DATA,
   the queue before the MTA assigns an identifier, or the session and
   senderAuth at outbound stages when the MTA did not retain them.  At
   the "transmit" stage, envelope and message describe the transaction
   about to be performed.  At the "dsn" stage, envelope, message, and
   rawMessage describe the DSN about to be sent; queue identifies the
   message the DSN reports on.

De Gennaro                 Expires 1 May 2027                  [Page 16]
Internet-Draft                  MTA Hooks                   October 2026

5.3.  Session

   A *Session* object describes an SMTP session.  For a message
   submitted by other means, such as the JSON Meta Application Protocol
   (JMAP) [RFC8621] or a local submission program, it describes that
   submission as far as applicable; for example, "auth" identifies the
   submitting account and "client" is null for local submission.

   id: String  Unique identifier of the session, stable across its
      stages and transactions.

   startedAt: UTCDate  When the connection was accepted.

   client: ClientInfo|null  The client.

   server: ServerInfo|null  The address the client connected to.

   tls: TlsInfo|null  TLS negotiated or attempted on the session, or
      null.

   auth: SmtpAuth|null  Authentication of the client, or null.

   counters: String[UnsignedInt]  Session counters and measurements,
      keyed by names from the registry in Section 11.3.3.  The MTA
      includes those it maintains.

   A *ClientInfo* object has the following properties:

   ip: String  Client IP address.

   port: UnsignedInt  Client port.

   ptr: String|null  Host name from the PTR record of the IP address.

   ehlo: String|null  Argument of the most recent EHLO or HELO command.

   asn: UnsignedInt|null  Autonomous System Number (ASN) of the IP
      address.

   country: String|null  Alpha-2 country code [ISO3166-1] of the IP
      address.

   activeConnections: UnsignedInt|null  Concurrent connections from this
      IP address.

   proxy: ProxyInfo|null  How the client identity was conveyed, if not
      taken from the socket.

De Gennaro                 Expires 1 May 2027                  [Page 17]
Internet-Draft                  MTA Hooks                   October 2026

   The client properties carry the effective client identity.  When it
   was conveyed by a proxy mechanism that the MTA trusts, "proxy"
   describes that mechanism; when the mechanism is not trusted, the MTA
   ignores it and the request contains the socket identity without a
   "proxy" property.

   A *ProxyInfo* object has the following properties:

   protocol: String  "proxy-v1" or "proxy-v2" for the PROXY protocol
      [PROXY], "xclient" for XCLIENT [XCLIENT], or "xforward" for
      XFORWARD [XFORWARD].

   ip: String  IP address of the proxy, the socket peer.

   port: UnsignedInt  Port of the proxy.

   overridden: String[]  Property paths of the session values that came
      from the proxy, for example "/session/client/ip" or
      "/session/server/port".

   A *ServerInfo* object has the following properties:

   name: String  Host name the MTA uses on this connection.

   ip: String  IP address the client connected to.

   port: UnsignedInt  Port the client connected to.

   A *TlsInfo* object has the following properties:

   version: String|null  TLS version: "TLSv1.2" or "TLSv1.3", or null if
      no handshake completed.

   cipher: String|null  Name of the cipher suite as listed in the IANA
      "TLS Cipher Suites" registry, or null if no handshake completed.

   peerCertificate: Certificate|null  The certificate presented by the
      peer: the client certificate at inbound stages, the server
      certificate at outbound stages.  Null if none was presented.

   policy: String|null  Outbound only: the policy type applied, as
      defined for "policy-type" in [RFC8460].

   failure: String|null  Outbound only: why TLS failed, as a "result-
      type" value of [RFC8460], or null.

   A *Certificate* object has the following properties:

De Gennaro                 Expires 1 May 2027                  [Page 18]
Internet-Draft                  MTA Hooks                   October 2026

   chain: Bytes[]  The certificates as presented, encoded with the
      Distinguished Encoding Rules (DER) [X.690], end-entity certificate
      first.

   verification: String  "pass" if the MTA verified the certificate,
      using PKIX [RFC5280] or DNS-Based Authentication of Named Entities
      (DANE) [RFC7672]; "none" if it did not attempt verification;
      otherwise why verification failed, as one of the "result-type"
      values "certificate-expired", "certificate-not-trusted",
      "certificate-host-mismatch", or "validation-failure" of [RFC8460].

   The certificate is reported whether or not it could be verified.  A
   certificate that did not verify is an unauthenticated assertion by
   the peer; it is useful for recognizing a peer, but not as proof of
   its identity.

   An *SmtpAuth* object has the following properties:

   login: String  Authenticated identity.

   method: String|null  Name of the Simple Authentication and Security
      Layer (SASL) [RFC4422] mechanism used with SMTP AUTH [RFC4954], or
      null for authentication by other means.

5.4.  Envelope and Recipients

   An *Envelope* object has the following properties:

   from: Address  The reverse-path.

   to: Recipient[]  The recipients.

   An *Address* object has the following properties:

   address: String|null  The address, or null for the null reverse-path.

   parameters: String[String|null]  All Extended SMTP (ESMTP) parameters
      given with the address, keyed by upper-case name, with null for a
      parameter without a value.  Values that their extension encodes as
      xtext, such as ENVID and the address in ORCPT [RFC3461], are
      decoded.  Examples are SIZE, BODY, SMTPUTF8, REQUIRETLS, RET,
      ENVID, NOTIFY, and ORCPT.

   A *Recipient* object has the properties of Address, where address is
   not null, and the following:

   status: String  See below.

De Gennaro                 Expires 1 May 2027                  [Page 19]
Internet-Draft                  MTA Hooks                   October 2026

   response: SmtpResponse|null  The reply that determined the status:
      the reply given to the RCPT command at inbound stages, or the
      reply of the next hop at outbound stages.

   decidedBy: String  Label of the registration that last changed the
      status, or "mta".

   expansion: String[]|null  Inbound only: the addresses this recipient
      resolves to after alias expansion and rewriting, when the MTA has
      already resolved it.

   originalRecipient: String|null  Outbound only: the address as given
      in RCPT TO, when this recipient resulted from expansion or
      rewriting.

   attempts: UnsignedInt  Outbound only: delivery attempts made for this
      recipient.

   nextAttemptAt: UTCDate|null  Outbound only: when the next attempt is
      scheduled.

   nextDsnAt: UTCDate|null  Outbound only: when the next delay DSN is
      scheduled.

   route: Route  Outbound only: the route used for the next attempt
      (Section 6.6).  Its null properties take the MTA's defaults.

   lastAttempt: Attempt|null  Outbound only: the most recent delivery
      attempt.

   The status of a recipient is one of:

De Gennaro                 Expires 1 May 2027                  [Page 20]
Internet-Draft                  MTA Hooks                   October 2026

    +===========+====================================================+
    | Status    | Meaning                                            |
    +===========+====================================================+
    | pending   | The recipient of the RCPT command being processed. |
    +-----------+----------------------------------------------------+
    | accepted  | Accepted at RCPT and not yet attempted.            |
    +-----------+----------------------------------------------------+
    | deferred  | Temporarily failed: a 4xx reply at RCPT, or an     |
    |           | outbound attempt to be retried.                    |
    +-----------+----------------------------------------------------+
    | failed    | Permanently failed: a 5xx reply at RCPT, or an     |
    |           | outbound failure for which a DSN is due.           |
    +-----------+----------------------------------------------------+
    | delivered | Delivered to the next hop.                         |
    +-----------+----------------------------------------------------+
    | discarded | Dropped without delivery and without a DSN.        |
    +-----------+----------------------------------------------------+

                        Table 2: Recipient Status

   At inbound stages, recipients appear in the order of their RCPT
   commands, including those rejected or deferred.  At the "rcpt" stage
   the last recipient is the one of the current command, with status
   "pending".  At outbound stages, the array contains the recipients
   that the invocation covers: those of the delivery job matched by the
   subscription, those of the transaction at the "transmit" stage, or
   the recipient of the DSN at the "dsn" stage.

   An *Attempt* object has the following properties:

   at: UTCDate  When the attempt was made.

   remote: RemoteHost|null  The host the MTA connected to, or null if no
      connection was made.

   sourceIp: String|null  The local IP address used.

   tls: TlsInfo|null  TLS on the connection, or null if TLS was not
      attempted.

   A *RemoteHost* object has the following properties:

   host: String  Host name of the next hop, such as the MX name.

   ip: String  IP address connected to.

   port: UnsignedInt  Port connected to.

De Gennaro                 Expires 1 May 2027                  [Page 21]
Internet-Draft                  MTA Hooks                   October 2026

5.5.  Message

   The "message" property is the Email object of Section 4 of [RFC8621],
   without the metadata properties of Section 4.1.1 of [RFC8621] other
   than "size", and with the "blobId" of each EmailBodyPart replaced by
   a "blob" property of type Bytes holding the part's content after
   decoding its Content-Transfer-Encoding [RFC2045].  The "rawMessage"
   property is the message octet for octet, which signature verification
   and archiving require.

   A scanner can request either or both.  Header changes are made
   through "/message/headers"; the convenience properties derived from
   headers, such as "subject" and "from", cannot be changed.  Body
   changes are made through "bodyValues" and "bodyStructure", or by
   replacing "rawMessage" with a complete new message.  A patch that
   changes both "message" and "rawMessage" is invalid.  After a change,
   the MTA presents both consistently to the following scanners.

5.6.  Sender Authentication

   A *SenderAuth* object has the following properties, each holding a
   result value of the corresponding Authentication-Results method
   [RFC8601], or null if not evaluated:

   iprev: String|null  Reverse DNS check of the client address (forward-
      confirmed reverse DNS).

   spfHelo: String|null  Sender Policy Framework (SPF) [RFC7208] for the
      HELO identity.

   spfMailFrom: String|null  SPF for the MAIL FROM identity.

   dkim: String|null  DomainKeys Identified Mail (DKIM) [RFC6376].

   dkim2: String|null  DKIM2 [I-D.ietf-dkim-dkim2-spec], with the result
      values of [I-D.gondwana-dkim2-authres].

   dmarc: String|null  Domain-based Message Authentication, Reporting,
      and Conformance (DMARC) [RFC9989].

5.7.  Queue

   A *Queue* object has the following properties:

   id: String  Identifier of the message in the MTA's queue.

   expiresAt: UTCDate|null  When the MTA gives up delivering the
      message.  Present at outbound stages.

De Gennaro                 Expires 1 May 2027                  [Page 22]
Internet-Draft                  MTA Hooks                   October 2026

   The queue identifier MUST be the same at every stage and every
   delivery job of the message, from "data" to the last "delivery" or
   "dsn", and MUST be unique within the MTA for the lifetime of the
   message.  Scanners can therefore keep the state they collected at
   inbound stages until expiresAt.

5.8.  Extension Data

   The "ext" object holds data outside this specification.  Each key is
   a namespace named by a reverse domain name controlled by its definer,
   for example "org.example.spam", and its value is an object whose
   content that definer specifies.

   Extension data is written by the MTA and by scanners, and exchanged
   like any other property: the subscription's properties decide which
   namespaces a scanner receives, and its grants decide which it can
   write.  A namespace written by one scanner and sent to another is how
   scanners pass results along a chain without adding header fields to
   the message.  Recipients of a request MUST ignore namespaces they do
   not understand.

   Extension data is kept with the message for as long as the message is
   in the MTA, so data written at the "data" stage is available again at
   the "route", "delivery", and "dsn" stages.  Data written before the
   MAIL command is kept with the session and copied to each of its
   transactions.  The MTA SHOULD limit the size of each namespace
   through the "max" of the grant.

5.9.  SMTP Response

   An *SmtpResponse* object has the following properties:

   code: UnsignedInt  The reply code.

   enhancedCode: String|null  The enhanced status code [RFC3463].

   message: String  The reply text.

   delayMs: UnsignedInt|null  Milliseconds the MTA waits before sending
      the reply.  At the "connect" stage this delays the greeting, which
      reveals clients that send commands before it.  The MTA MUST cap
      it, normally through the "max" of the grant, below the 5-minute
      timeouts that SMTP clients apply (Section 4.5.3.2 of [RFC5321]).

De Gennaro                 Expires 1 May 2027                  [Page 23]
Internet-Draft                  MTA Hooks                   October 2026

5.10.  EHLO Capabilities

   At the "ehlo" stage, a scanner can only withhold what the MTA offers:
   it may remove keywords, remove space-separated parameters from a
   value, or lower a numeric value such as that of SIZE.  Other changes
   to "capabilities" are invalid regardless of grants.  This allows, for
   example, offering AUTH only on some networks or withholding
   PIPELINING and CHUNKING from suspicious clients.

5.11.  Examples

   An inbound request at the "data" stage, with the message abbreviated:

   POST /v1/hooks/reg-7a3b9c2e HTTP/1.1
   Host: scanner.example.com
   Content-Type: application/json
   Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
   MTA-Hooks-Registration: reg-7a3b9c2e
   MTA-Hooks-Request-Id: 01JBDX3X7F4Y5N6Q9R2S3T4U5V
   MTA-Hooks-Context: 01JBDX3TQ8V0D6H1E2F3G4H5J6

   {
     "stage": "data",
     "action": "accept",
     "decidedBy": "mta",
     "response": {"code": 250, "enhancedCode": "2.0.0",
                  "message": "Queued as 4F2A31B0C9"},
     "timestamp": "2026-10-28T16:30:00Z",
     "timeoutMs": 4200,
     "session": {
       "id": "01JBDX3TQ8V0D6H1E2F3G4H5J6",
       "startedAt": "2026-10-28T16:29:58Z",
       "client": {"ip": "192.0.2.100", "port": 54321,
                  "ptr": "mail.example.org",
                  "ehlo": "mail.example.org", "asn": 64496},
       "server": {"name": "mx1.example.com",
                  "ip": "198.51.100.25", "port": 25},
       "tls": {"version": "TLSv1.3",
               "cipher": "TLS_AES_256_GCM_SHA384",
               "peerCertificate": null},
       "auth": null,
       "counters": {"commands": 5, "transactions": 1,
                    "pregreetBytes": 0}
     },
     "envelope": {
       "from": {"address": "sender@example.org",
                "parameters": {"SIZE": "2048", "BODY": "8BITMIME"}},
       "to": [

De Gennaro                 Expires 1 May 2027                  [Page 24]
Internet-Draft                  MTA Hooks                   October 2026

         {"address": "bob@example.com", "parameters": {},
          "status": "accepted", "decidedBy": "mta",
          "response": {"code": 250, "message": "OK"}},
         {"address": "nobody@example.com", "parameters": {},
          "status": "failed", "decidedBy": "mta",
          "response": {"code": 550, "enhancedCode": "5.1.1",
                       "message": "User unknown"}}
       ]
     },
     "message": {
       "size": 2048,
       "subject": "Meeting Tomorrow",
       "headers": [
         {"name": "From", "value": " Sender <sender@example.org>"},
         {"name": "To", "value": " bob@example.com"},
         {"name": "Subject", "value": " Meeting Tomorrow"}
       ],
       "bodyValues": {
         "1": {"value": "Let's meet tomorrow at 10am.",
               "isEncodingProblem": false, "isTruncated": false}
       }
     },
     "senderAuth": {"iprev": "pass", "spfMailFrom": "pass",
                    "dkim": "pass", "dkim2": "none", "dmarc": "pass"}
   }

   An outbound request at the "route" stage:

De Gennaro                 Expires 1 May 2027                  [Page 25]
Internet-Draft                  MTA Hooks                   October 2026

   {
     "stage": "route",
     "timestamp": "2026-10-28T17:00:00Z",
     "timeoutMs": 2000,
     "queue": {"id": "4F2A31B0C9", "expiresAt": "2026-11-02T16:30:00Z"},
     "envelope": {
       "from": {"address": "sales@example.com", "parameters": {}},
       "to": [
         {"address": "a@partner.example", "parameters": {},
          "status": "deferred", "decidedBy": "mta",
          "response": {"code": 451, "enhancedCode": "4.4.1",
                       "message": "Connection timed out"},
          "attempts": 1, "nextAttemptAt": "2026-10-28T17:00:00Z",
          "nextDsnAt": "2026-10-29T16:30:00Z",
          "route": {"name": null},
          "lastAttempt": {
            "at": "2026-10-28T16:45:00Z",
            "remote": {"host": "mx.partner.example",
                       "ip": "203.0.113.7", "port": 25},
            "sourceIp": "198.51.100.25", "tls": null}}
       ]
     },
     "ext": {"org.example.spam": {"score": 0.4}}
   }

6.  Hook Response

6.1.  Structure

   A scanner answers with 200 and a JSON Patch [RFC6902] in the
   negotiated serialization, or with 204 and no body when it has no
   changes.  An empty patch also means no changes.  In JSON the Content-
   Type is "application/json-patch+json"; in CBOR it is "application/
   cbor" and the patch is the CBOR encoding of the same array.

   The patch is applied to the hook request as held by the MTA, which
   can include properties that were not sent.  A scanner can, for
   example, add a header field with "add" at "/message/headers/0"
   without receiving the message, or append a recipient at "/envelope/
   to/-".  In that state, every property defined for an object that
   exists at the stage is present, with the value null when it has none,
   so "replace" and "test" behave the same whether or not a null
   property was sent.  The "/envelope/to" array is the one the request
   carries, in the same order.

De Gennaro                 Expires 1 May 2027                  [Page 26]
Internet-Draft                  MTA Hooks                   October 2026

6.2.  Validation and Application

   The MTA applies the patch to a copy of the state.  The patch is valid
   only if:

   1.  it applies successfully according to [RFC6902], including all
       "test" operations;

   2.  the "from" of every "copy" and "move" operation, and the "path"
       of every "test" operation, is covered by the subscription's
       properties;

   3.  every location whose value differs between the original and the
       resulting state is covered by a grant of the subscription, and
       satisfies the "values" and "max" of the grant that applies
       (Section 4.1.2);

   4.  it does not change "stage", "timestamp", "timeoutMs", any
       "decidedBy", or a non-null "traceId", which no grant covers;

   5.  the resulting state respects the ordering of Section 6.3 unless
       that grant relaxes it; and

   6.  the resulting state is otherwise valid: values have the types
       defined in this document, actions and statuses are permitted at
       the stage (Section 6.4, Section 6.5), at most one of "message"
       and "rawMessage" changed, and header fields, addresses, and
       message content are well formed (Section 9.5).

   Because validation compares states, it does not depend on which
   operations produced the result.  A valid patch replaces the state,
   after which the MTA sets "decidedBy" of the action and of each
   changed recipient to the label of the registration.  An invalid patch
   is discarded as a whole, is logged with the request identifier, and
   counts as a failure of the scanner (Section 7.3).

6.3.  Ordering

   Some properties are ordered from permissive to strict.  Without
   "relax", a scanner may only keep them, move them in the strict
   direction, or move them between equally strict values:

De Gennaro                 Expires 1 May 2027                  [Page 27]
Internet-Draft                  MTA Hooks                   October 2026

   +=======================+===========================================+
   | Property              | Permissive to strict                      |
   +=======================+===========================================+
   | /action               | accept, quarantine, then                  |
   |                       | reject, discard, and disconnect           |
   |                       | (all equally strict)                      |
   +-----------------------+-------------------------------------------+
   | /envelope/to/*/route/ | none, opportunistic, required,            |
   | tls                   | verified                                  |
   +-----------------------+-------------------------------------------+

                        Table 3: Ordered Properties

   A null "tls" ranks as the requirement the MTA would otherwise apply.
   With "relax", the scanner may, for example, accept a message that the
   MTA's own policy rejected or that an earlier scanner quarantined.
   Such a grant makes the scanner the final authority for the property
   (Section 9.2).  When several scanners with "relax" change the same
   property, the last one in the chain prevails.

6.4.  Actions

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

De Gennaro                 Expires 1 May 2027                  [Page 28]
Internet-Draft                  MTA Hooks                   October 2026

      +============+===============================================+
      | Action     | Meaning                                       |
      +============+===============================================+
      | accept     | Continue normally.                            |
      +------------+-----------------------------------------------+
      | quarantine | Accept the message and quarantine it instead  |
      |            | of delivering it.  Not permitted at the       |
      |            | "connect" and "ehlo" stages.                  |
      +------------+-----------------------------------------------+
      | reject     | Send "/response", a 4xx or 5xx reply.  At the |
      |            | "connect" stage it replaces the greeting and  |
      |            | MUST be a 554 reply (Section 3.1 of           |
      |            | [RFC5321]); a temporary refusal of the        |
      |            | connection is expressed with "disconnect".    |
      |            | At the "rcpt" stage it rejects the recipient. |
      +------------+-----------------------------------------------+
      | discard    | At the "mail" and "data" stages: accept the   |
      |            | message and drop it.  At the "rcpt" stage:    |
      |            | accept the recipient and set it to            |
      |            | "discarded".  Not permitted at the "connect"  |
      |            | and "ehlo" stages.                            |
      +------------+-----------------------------------------------+
      | disconnect | Send "/response", a 421 reply (Section 3.8 of |
      |            | [RFC5321]), and close the connection.         |
      +------------+-----------------------------------------------+

                             Table 4: Actions

   The terminal actions are "reject", "discard", and "disconnect": after
   a scanner sets one, the MTA invokes no further scanners for that
   stage.  At the "rcpt" stage, "reject" and "discard" apply only to the
   current recipient, and processing of the transaction continues.

   At the start of each inbound stage, the action is the one the MTA's
   own policy assigns to the command, except that a "quarantine" set at
   the "mail" or "rcpt" stage, or a "discard" set at the "mail" stage,
   applies to the message and is kept for the later stages of the
   transaction.  The chain runs even if that initial action is terminal,
   so that a scanner with "relax" can override it.

   Outbound stages have no action: their outcome is the status of each
   recipient (Section 6.5).  The chain ends early when every recipient
   of the invocation has been discarded.

De Gennaro                 Expires 1 May 2027                  [Page 29]
Internet-Draft                  MTA Hooks                   October 2026

   "accept", "quarantine", and "discard" require a 2xx reply, or the
   intermediate reply SMTP requires for the command; "reject" requires a
   4xx or 5xx reply; and "disconnect" requires a 421 reply.  If
   "/response" does not satisfy this, the MTA uses its default reply for
   the action.

6.5.  Recipient Changes

   Recipients cannot be removed from "/envelope/to"; a recipient is
   dropped by setting its status to "discarded".  Recipients can be
   added at the "data" stage, with status "accepted".  The status of a
   recipient can be changed as follows:

       +===========================+===========+===================+
       | Stage                     | From      | To                |
       +===========================+===========+===================+
       | data                      | accepted  | discarded         |
       +---------------------------+-----------+-------------------+
       | route, transmit, delivery | accepted, | deferred, failed, |
       |                           | deferred  | discarded         |
       +---------------------------+-----------+-------------------+
       | delivery                  | failed    | discarded         |
       +---------------------------+-----------+-------------------+
       | dsn                       | accepted  | discarded         |
       +---------------------------+-----------+-------------------+

                     Table 5: Recipient Status Changes

   Setting a failed recipient to "discarded" suppresses its DSN, and
   discarding the recipient of a DSN at the "dsn" stage suppresses that
   DSN.  A scanner that sets a recipient to "failed" or "deferred" MAY
   also set its "response", which the MTA reports in the DSN; otherwise
   the MTA uses a default reply.  At the "route" and "delivery" stages,
   "nextAttemptAt" and "nextDsnAt" can be changed for recipients that
   remain deferred.

   Discarding a message or a recipient after accepting it, and
   suppressing a DSN, depart from Section 6.1 of [RFC5321] and are
   subject to the cautions of Section 6.2 of [RFC5321].  MTAs SHOULD log
   them, and operators SHOULD grant them only for unwanted mail or where
   the sender is notified by other means.

   At the "rcpt" stage the outcome of the current recipient is set
   through "/action" and "/response", and the MTA records it in the
   recipient's status.  Per-recipient outcomes after the DATA command
   are limited to "discarded" because SMTP gives a single reply to the
   message.

De Gennaro                 Expires 1 May 2027                  [Page 30]
Internet-Draft                  MTA Hooks                   October 2026

6.6.  Routing

   At the "route" stage, a scanner can choose how each recipient of the
   job is delivered by changing its "route".  A *Route* object has the
   following properties:

   name: String|null  A route the MTA has configured, such as a queue or
      transport name, carrying its own next hop, TLS, source address,
      credential, and throttling settings.  The names a scanner may use
      are given by the "values" of its grant.

   nextHop: String|null  Host name or IP address to deliver to,
      overriding the route's next hop.

   port: UnsignedInt|null  Port to connect to.

   mxLookup: Boolean|null  Whether "nextHop" is a domain whose MX
      records are resolved (true) or a host to connect to directly
      (false).

   sourceIp: String|null  Local IP address to connect from.

   tls: String|null  Minimum TLS requirement: "none", "opportunistic",
      "required" (TLS without authentication of the server), or
      "verified" (TLS with the server authenticated by DANE [RFC7672],
      SMTP MTA Strict Transport Security (MTA-STS) [RFC8461], or PKIX
      [RFC5280]).

   A null property takes its value from the named route, or from the
   MTA's default routing when "name" is null as well.  Two routes are
   different when any of their properties differ after null properties
   have been resolved.  Because each property can be granted separately,
   an MTA can let a scanner choose only among predefined routes, or give
   it complete control over next hops, source addresses, and TLS.
   Credentials are never carried in a route; they belong to the named
   route.

   The MTA MUST NOT deliver recipients with different routes in the same
   SMTP transaction.  Otherwise, how recipients are batched remains
   implementation-defined.  A route change persists for the recipient
   until changed again, and the "route" stage runs before every attempt,
   so a scanner can fail over based on "attempts" and "lastAttempt".
   The "delivery" stage runs once for the job, after all of its partial
   deliveries.

   A grant that relaxes "tls" lets the scanner lower the requirement
   below the policy published by the destination, as a "TLS-Required:
   No" header field does (Section 4.2.2 of [RFC8689]), for example to

De Gennaro                 Expires 1 May 2027                  [Page 31]
Internet-Draft                  MTA Hooks                   October 2026

   deliver a TLS report to a domain whose TLS is broken; lowering it to
   "none" also skips STARTTLS.  Like that header field, it does not
   apply to a message sent with the REQUIRETLS parameter (Section 4.1 of
   [RFC8689]).  The MTA SHOULD log every route change together with the
   registration label and the recipient, and MUST apply its loop
   detection to scanner-chosen next hops.

6.7.  Transmission

   The "transmit" stage runs once for each SMTP transaction, after the
   MTA has grouped the recipients of the job into transactions, so the
   request carries the exact reverse-path and recipients that will be
   sent.  A scanner can change the message for this transaction only,
   for example to add signatures that cover the envelope of each hop,
   such as DKIM2 signatures [I-D.ietf-dkim-dkim2-spec].  Within the
   envelope, only recipient status can be changed.  Status changes are
   kept; changes to the message apply to this transaction only and are
   not kept in the queue.  The stage runs again for every attempt, so
   signatures carry the time and envelope of the attempt that uses them.
   A DSN is changed the same way at the "dsn" stage.

6.8.  Examples

   Add header fields and pass a score to later scanners:

   [
     {"op": "add", "path": "/message/headers/0",
      "value": {"name": "X-Spam-Status", "value": " No, score=0.4"}},
     {"op": "add", "path": "/ext/org.example.spam",
      "value": {"score": 0.4, "symbols": ["DKIM_VALID"]}}
   ]

   Reject a message:

   [
     {"op": "replace", "path": "/action", "value": "reject"},
     {"op": "replace", "path": "/response",
      "value": {"code": 550, "enhancedCode": "5.7.1",
                "message": "Message rejected: virus detected"}}
   ]

   Slow down a suspicious client at the "connect" stage:

   [
     {"op": "add", "path": "/response/delayMs", "value": 15000}
   ]

De Gennaro                 Expires 1 May 2027                  [Page 32]
Internet-Draft                  MTA Hooks                   October 2026

   Route the first recipient through a virtual private network (VPN),
   but only if no other scanner has already routed it:

   [
     {"op": "test", "path": "/envelope/to/0/route/name",
      "value": null},
     {"op": "replace", "path": "/envelope/to/0/route/name",
      "value": "vpn-partner"}
   ]

   Drop a failed recipient without a DSN at the "delivery" stage:

   [
     {"op": "replace", "path": "/envelope/to/2/status",
      "value": "discarded"}
   ]

6.9.  Error Responses

   A scanner that cannot process a request returns a 4xx or 5xx status
   with the error body of Appendix A:

   *  401: Credentials missing or invalid.  The MTA MUST NOT retry with
      the same credentials.

   *  403: Credentials not authorized for the registration.  The MTA
      MUST NOT retry.

   *  404: Registration unknown.  The MTA registers again.

   *  413: Request body larger than the scanner accepts.  The MTA MUST
      NOT retry it.

   *  422: The request uses unsupported properties or values.  The MTA
      MUST NOT retry it.

   *  429: Rate limited.  The MTA SHOULD NOT send further requests under
      the registration until the time given by Retry-After, if any, and
      applies onFailure to invocations meanwhile.

   *  5xx: Transient error.  The MTA MAY retry (Section 7.2).

   Any other status is a failure that the MTA MUST NOT retry.  MTAs
   SHOULD alert their operators to 401, 403, 413, and 422, which persist
   until the configuration changes; with "tempfail" they can hold mail
   until the message expires.

De Gennaro                 Expires 1 May 2027                  [Page 33]
Internet-Draft                  MTA Hooks                   October 2026

7.  Transport

7.1.  HTTP and TLS

   Implementations MUST support HTTP/1.1 [RFC9112] and SHOULD support
   HTTP/2 [RFC9113]; they MAY support HTTP/3 [RFC9114].  MTAs SHOULD
   reuse connections to a scanner.

   All MTA Hooks traffic MUST use TLS 1.2 or later and follow the
   recommendations of [RFC9325]; implementations SHOULD support TLS 1.3
   [RFC9846].  The MTA MUST validate the scanner's certificate; private
   certificate authorities may be configured, but validation SHOULD NOT
   be disabled.

7.2.  Retries

   On a network error or a 5xx status, the MTA MAY retry within the
   request's timeoutMs, with exponential backoff and jitter.

7.3.  Failure Handling

   A scanner fails when it does not answer within timeoutMs, when all
   retries fail, when it returns a status other than 200 or 204, or when
   its patch is invalid (Section 6.2).  The MTA then leaves the state
   unchanged and acts according to the subscription's onFailure:

   continue:  Proceed with the next scanner, as if the scanner had made
      no changes.

   tempfail:  At the "connect" and "ehlo" stages, answer with a 421
      4.3.0 reply and close the connection; at the other inbound stages,
      answer the current command with a 451 4.3.0 reply (Section 4.3.2
      of [RFC5321]).  At the "route" and "transmit" stages, leave the
      invocation's accepted and deferred recipients deferred and retry
      them later; a transaction skipped this way does not count as an
      attempt.  At the "dsn" stage, keep the DSN queued and retry it
      later.  At the "delivery" stage, outcomes are already final, so
      the failure only means the scanner's changes are lost; a scanner
      that must reliably suppress DSNs does so at the "dsn" stage.

   "tempfail" is appropriate where a missing decision must not let mail
   through, such as mandatory routing or mandatory archiving.  It can
   stop the affected mail flow while the scanner is unavailable, so
   scanners used this way SHOULD be deployed redundantly, and
   subscription filters SHOULD limit it to the mail that requires it.
   MTAs SHOULD temporarily stop calling a scanner that fails repeatedly,
   and apply its onFailure in the meantime.

De Gennaro                 Expires 1 May 2027                  [Page 34]
Internet-Draft                  MTA Hooks                   October 2026

8.  Implementation Status

   Note to the RFC Editor: please remove this section, and the reference
   to [RFC7942], before publication.

   This section records the status of known implementations of the
   protocol defined by this specification at the time of posting of this
   Internet-Draft, and is based on a proposal described in [RFC7942].
   The description of implementations in this section is intended to
   assist the IETF in its decision processes in progressing drafts to
   RFCs.  Please note that the listing of any individual implementation
   here does not imply endorsement by the IETF.  Furthermore, no effort
   has been spent to verify the information presented here that was
   supplied by IETF contributors.  This is not intended as, and must not
   be construed to be, a catalog of available implementations or their
   features.  Readers are advised to note that other implementations may
   exist.

   Stalwart Mail Server:  Organization: Stalwart Labs LLC, with which
      the author is affiliated.  Role: MTA.  Coverage: an earlier
      revision of this specification (-01).  Licensing: AGPL-3.0 for the
      open-source edition.  Contact: mauro@stalw.art.

9.  Security Considerations

9.1.  Trust Model

   The MTA acts only as an HTTP client and exposes no endpoint to
   scanners.  This removes attacks such as registration floods against
   the MTA and request forgery through callbacks.  The scanner operator
   decides which MTAs may register and issues credentials; the MTA
   operator decides which scanners to call and what each may change.

9.2.  Authority of Scanners

   A scanner can only change what its grants cover, and by default only
   toward stricter processing.  Grants that relax ordered properties,
   change routes, or rewrite content make the scanner part of the
   trusted base for those decisions: a compromised scanner with full
   routing authority can redirect or exfiltrate mail, and one with
   "relax" on "/action" can let malicious mail through.  Operators
   SHOULD give such grants only to scanners they trust accordingly,
   limit them with subscription filters, and log their use.  The MTA
   MUST enforce grants itself and MUST NOT rely on scanners respecting
   them.

De Gennaro                 Expires 1 May 2027                  [Page 35]
Internet-Draft                  MTA Hooks                   October 2026

   Scanner-chosen next hops and ports can also direct the MTA's
   connections at internal services.  An MTA SHOULD restrict them by
   configuration, for example through the "values" of the grant on
   "nextHop", and MUST apply to them the same address restrictions it
   applies to next hops found in the DNS.

9.3.  Credentials

   Credentials SHOULD be issued per MTA instance, rotated, revoked when
   an instance is decommissioned, and kept out of logs and diagnostics.
   Bearer tokens MUST have at least 128 bits of entropy, and the
   security considerations of [RFC6750] apply.  Scanners SHOULD apply
   authorization per MTA identity at registration and SHOULD rate limit
   registrations per identity.  The discovery document is
   unauthenticated and SHOULD be rate limited.

9.4.  Denial of Service

   Slow or failing scanners can tie up MTA resources.  MTAs MUST enforce
   timeouts and SHOULD limit concurrent requests per scanner.  Delayed
   replies hold SMTP connections open; besides capping delayMs
   (Section 5.9), the MTA SHOULD limit how many delayed replies it
   serves at once.  Recipients added at the "data" stage can amplify a
   message, so MTAs SHOULD limit how many a patch may add.  Scanners
   SHOULD limit request rate, concurrency, and body size per
   registration.

9.5.  Injection

   The MTA MUST validate everything a patch writes: header field names
   and values MUST NOT allow injection of a carriage return (CR) or line
   feed (LF), or malformed encoded words [RFC2047]; added or changed
   addresses MUST be syntactically valid and subject to the MTA's relay
   and recipient policy; and body changes MUST produce well-formed MIME
   [RFC2045].

9.6.  Unverified Data

   Certificates that did not verify, proxy information, and client-
   supplied values such as the EHLO argument are assertions by the peer.
   Scanners MUST NOT treat them as authenticated identities.  A trace
   identifier received from outside the MTA's administrative domain can
   be chosen by an attacker to link unrelated mail, so MTAs MUST NOT
   accept one from untrusted sources.

De Gennaro                 Expires 1 May 2027                  [Page 36]
Internet-Draft                  MTA Hooks                   October 2026

10.  Privacy Considerations

   Hook requests can contain message content, addresses, client IP
   addresses, and authentication identities.  MTAs SHOULD send only the
   properties a scanner needs, and the subscription mechanism makes this
   possible per stage and per part of the mail stream.  Scanner
   operators SHOULD avoid logging content and personal data, and SHOULD
   limit retention to what their function requires.

11.  IANA Considerations

11.1.  Well-Known URI

   IANA is requested to register the following well-known URI [RFC8615]:

   *  URI suffix: mta-hooks

   *  Change controller: IETF

   *  Specification document: this document

   *  Status: permanent

11.2.  HTTP Field Names

   IANA is requested to register the following field names in the
   "Hypertext Transfer Protocol (HTTP) Field Name Registry"
   (Section 16.3.1 of [RFC9110]), with status "permanent", no structured
   type, and this document as the reference: MTA-Hooks-Registration,
   MTA-Hooks-Request-Id, MTA-Hooks-Context.

11.3.  MTA Hooks Registries

   IANA is requested to create a new registry group named "MTA Hooks"
   containing the four registries below.  For all of them, new
   registrations require Specification Required [RFC8126], and the
   change controller of the initial entries is the IETF.

   The designated experts SHOULD check that a name is a short identifier
   in the style of the existing entries and does not duplicate the
   semantics of an existing entry.  For a stage, they SHOULD check that
   the specification defines when it is invoked, what the request
   contains, and which changes are permitted.  For an action, they
   SHOULD check that its meaning is defined at every stage where it is
   permitted, and its place in the ordering of Section 6.3.

De Gennaro                 Expires 1 May 2027                  [Page 37]
Internet-Draft                  MTA Hooks                   October 2026

11.3.1.  MTA Hooks Stages

   Each entry gives the name, the direction (inbound or outbound), when
   the stage is invoked, the reference, and the change controller.

        +==========+===========+======================+===========+
        | Name     | Direction | Invoked              | Reference |
        +==========+===========+======================+===========+
        | connect  | inbound   | Client connected,    | RFC XXXX  |
        |          |           | before the greeting  |           |
        +----------+-----------+----------------------+-----------+
        | ehlo     | inbound   | After each EHLO or   | RFC XXXX  |
        |          |           | HELO command         |           |
        +----------+-----------+----------------------+-----------+
        | mail     | inbound   | After each MAIL      | RFC XXXX  |
        |          |           | command              |           |
        +----------+-----------+----------------------+-----------+
        | rcpt     | inbound   | After each RCPT      | RFC XXXX  |
        |          |           | command              |           |
        +----------+-----------+----------------------+-----------+
        | data     | inbound   | After the message    | RFC XXXX  |
        |          |           | content              |           |
        +----------+-----------+----------------------+-----------+
        | route    | outbound  | Before each delivery | RFC XXXX  |
        |          |           | attempt of a job     |           |
        +----------+-----------+----------------------+-----------+
        | transmit | outbound  | Before each SMTP     | RFC XXXX  |
        |          |           | transaction          |           |
        +----------+-----------+----------------------+-----------+
        | delivery | outbound  | After the attempts   | RFC XXXX  |
        |          |           | of a job             |           |
        +----------+-----------+----------------------+-----------+
        | dsn      | outbound  | Before sending a     | RFC XXXX  |
        |          |           | generated DSN        |           |
        +----------+-----------+----------------------+-----------+

                                  Table 6

11.3.2.  MTA Hooks Actions

   Each entry gives the name, whether the action is terminal, its rank
   in the ordering of Section 6.3 (higher is stricter), the reference,
   and the change controller.  The meaning of each initial entry at each
   stage is given in Table 4.

De Gennaro                 Expires 1 May 2027                  [Page 38]
Internet-Draft                  MTA Hooks                   October 2026

               +============+==========+======+===========+
               | Name       | Terminal | Rank | Reference |
               +============+==========+======+===========+
               | accept     | no       | 0    | RFC XXXX  |
               +------------+----------+------+-----------+
               | quarantine | no       | 1    | RFC XXXX  |
               +------------+----------+------+-----------+
               | reject     | yes      | 2    | RFC XXXX  |
               +------------+----------+------+-----------+
               | discard    | yes      | 2    | RFC XXXX  |
               +------------+----------+------+-----------+
               | disconnect | yes      | 2    | RFC XXXX  |
               +------------+----------+------+-----------+

                                 Table 7

11.3.3.  MTA Hooks Session Counters

   Each entry gives the name, its meaning, the reference, and the change
   controller.  The reference of every initial entry is RFC XXXX.

De Gennaro                 Expires 1 May 2027                  [Page 39]
Internet-Draft                  MTA Hooks                   October 2026

       +======================+===================================+
       | Name                 | Meaning                           |
       +======================+===================================+
       | commands             | SMTP commands received            |
       +----------------------+-----------------------------------+
       | errors               | Commands answered with a 4xx or   |
       |                      | 5xx reply                         |
       +----------------------+-----------------------------------+
       | invalidCommands      | Commands that were unknown or     |
       |                      | syntactically invalid             |
       +----------------------+-----------------------------------+
       | rsets                | RSET commands                     |
       +----------------------+-----------------------------------+
       | transactions         | MAIL commands accepted, including |
       |                      | the current transaction           |
       +----------------------+-----------------------------------+
       | messagesAccepted     | Messages accepted for delivery    |
       +----------------------+-----------------------------------+
       | messagesRejected     | Messages rejected after DATA      |
       +----------------------+-----------------------------------+
       | pregreetBytes        | Octets the client sent before the |
       |                      | greeting                          |
       +----------------------+-----------------------------------+
       | pipeliningViolations | Commands sent ahead of a reply    |
       |                      | without PIPELINING, or across a   |
       |                      | synchronization point             |
       +----------------------+-----------------------------------+
       | bareLineEnds         | Lines ended by a bare LF or bare  |
       |                      | CR                                |
       +----------------------+-----------------------------------+
       | firstCommandDelayMs  | Milliseconds between the greeting |
       |                      | and the first command             |
       +----------------------+-----------------------------------+

                    Table 8: Initial Session Counters

11.3.4.  MTA Hooks Error Codes

   Each entry gives the code, the HTTP status it is used with, its
   meaning, the reference, and the change controller.  The initial
   entries are those of Table 9, each with the reference RFC XXXX.

12.  References

12.1.  Normative References

De Gennaro                 Expires 1 May 2027                  [Page 40]
Internet-Draft                  MTA Hooks                   October 2026

   [ISO3166-1]
              ISO, "Codes for the representation of names of countries
              and their subdivisions -- Part 1: Country code",
              ISO 3166-1:2020, 2020.

   [RFC2045]  Freed, N. and N. Borenstein, "Multipurpose Internet Mail
              Extensions (MIME) Part One: Format of Internet Message
              Bodies", RFC 2045, DOI 10.17487/RFC2045, November 1996,
              <https://www.rfc-editor.org/rfc/rfc2045>.

   [RFC2047]  Moore, K., "MIME (Multipurpose Internet Mail Extensions)
              Part Three: Message Header Extensions for Non-ASCII Text",
              RFC 2047, DOI 10.17487/RFC2047, November 1996,
              <https://www.rfc-editor.org/rfc/rfc2047>.

   [RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
              Requirement Levels", BCP 14, RFC 2119,
              DOI 10.17487/RFC2119, March 1997,
              <https://www.rfc-editor.org/rfc/rfc2119>.

   [RFC3339]  Klyne, G. and C. Newman, "Date and Time on the Internet:
              Timestamps", RFC 3339, DOI 10.17487/RFC3339, July 2002,
              <https://www.rfc-editor.org/rfc/rfc3339>.

   [RFC3461]  Moore, K., "Simple Mail Transfer Protocol (SMTP) Service
              Extension for Delivery Status Notifications (DSNs)",
              RFC 3461, DOI 10.17487/RFC3461, January 2003,
              <https://www.rfc-editor.org/rfc/rfc3461>.

   [RFC3463]  Vaudreuil, G., "Enhanced Mail System Status Codes",
              RFC 3463, DOI 10.17487/RFC3463, January 2003,
              <https://www.rfc-editor.org/rfc/rfc3463>.

   [RFC3986]  Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
              Resource Identifier (URI): Generic Syntax", STD 66,
              RFC 3986, DOI 10.17487/RFC3986, January 2005,
              <https://www.rfc-editor.org/rfc/rfc3986>.

   [RFC4291]  Hinden, R. and S. Deering, "IP Version 6 Addressing
              Architecture", RFC 4291, DOI 10.17487/RFC4291, February
              2006, <https://www.rfc-editor.org/rfc/rfc4291>.

   [RFC4422]  Melnikov, A., Ed. and K. Zeilenga, Ed., "Simple
              Authentication and Security Layer (SASL)", RFC 4422,
              DOI 10.17487/RFC4422, June 2006,
              <https://www.rfc-editor.org/rfc/rfc4422>.

De Gennaro                 Expires 1 May 2027                  [Page 41]
Internet-Draft                  MTA Hooks                   October 2026

   [RFC4632]  Fuller, V. and T. Li, "Classless Inter-domain Routing
              (CIDR): The Internet Address Assignment and Aggregation
              Plan", BCP 122, RFC 4632, DOI 10.17487/RFC4632, August
              2006, <https://www.rfc-editor.org/rfc/rfc4632>.

   [RFC4648]  Josefsson, S., "The Base16, Base32, and Base64 Data
              Encodings", RFC 4648, DOI 10.17487/RFC4648, October 2006,
              <https://www.rfc-editor.org/rfc/rfc4648>.

   [RFC4954]  Siemborski, R., Ed. and A. Melnikov, Ed., "SMTP Service
              Extension for Authentication", RFC 4954,
              DOI 10.17487/RFC4954, July 2007,
              <https://www.rfc-editor.org/rfc/rfc4954>.

   [RFC5234]  Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax
              Specifications: ABNF", STD 68, RFC 5234,
              DOI 10.17487/RFC5234, January 2008,
              <https://www.rfc-editor.org/rfc/rfc5234>.

   [RFC5280]  Cooper, D., Santesson, S., Farrell, S., Boeyen, S.,
              Housley, R., and W. Polk, "Internet X.509 Public Key
              Infrastructure Certificate and Certificate Revocation List
              (CRL) Profile", RFC 5280, DOI 10.17487/RFC5280, May 2008,
              <https://www.rfc-editor.org/rfc/rfc5280>.

   [RFC5321]  Klensin, J., "Simple Mail Transfer Protocol", RFC 5321,
              DOI 10.17487/RFC5321, October 2008,
              <https://www.rfc-editor.org/rfc/rfc5321>.

   [RFC5322]  Resnick, P., Ed., "Internet Message Format", RFC 5322,
              DOI 10.17487/RFC5322, October 2008,
              <https://www.rfc-editor.org/rfc/rfc5322>.

   [RFC6750]  Jones, M. and D. Hardt, "The OAuth 2.0 Authorization
              Framework: Bearer Token Usage", RFC 6750,
              DOI 10.17487/RFC6750, October 2012,
              <https://www.rfc-editor.org/rfc/rfc6750>.

   [RFC6901]  Bryan, P., Ed., Zyp, K., and M. Nottingham, Ed.,
              "JavaScript Object Notation (JSON) Pointer", RFC 6901,
              DOI 10.17487/RFC6901, April 2013,
              <https://www.rfc-editor.org/rfc/rfc6901>.

   [RFC6902]  Bryan, P., Ed. and M. Nottingham, Ed., "JavaScript Object
              Notation (JSON) Patch", RFC 6902, DOI 10.17487/RFC6902,
              April 2013, <https://www.rfc-editor.org/rfc/rfc6902>.

De Gennaro                 Expires 1 May 2027                  [Page 42]
Internet-Draft                  MTA Hooks                   October 2026

   [RFC7672]  Dukhovni, V. and W. Hardaker, "SMTP Security via
              Opportunistic DNS-Based Authentication of Named Entities
              (DANE) Transport Layer Security (TLS)", RFC 7672,
              DOI 10.17487/RFC7672, October 2015,
              <https://www.rfc-editor.org/rfc/rfc7672>.

   [RFC8126]  Cotton, M., Leiba, B., and T. Narten, "Guidelines for
              Writing an IANA Considerations Section in RFCs", BCP 26,
              RFC 8126, DOI 10.17487/RFC8126, June 2017,
              <https://www.rfc-editor.org/rfc/rfc8126>.

   [RFC8174]  Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
              2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
              May 2017, <https://www.rfc-editor.org/rfc/rfc8174>.

   [RFC8259]  Bray, T., Ed., "The JavaScript Object Notation (JSON) Data
              Interchange Format", STD 90, RFC 8259,
              DOI 10.17487/RFC8259, December 2017,
              <https://www.rfc-editor.org/rfc/rfc8259>.

   [RFC8460]  Margolis, D., Brotman, A., Ramakrishnan, B., Jones, J.,
              and M. Risher, "SMTP TLS Reporting", RFC 8460,
              DOI 10.17487/RFC8460, September 2018,
              <https://www.rfc-editor.org/rfc/rfc8460>.

   [RFC8461]  Margolis, D., Risher, M., Ramakrishnan, B., Brotman, A.,
              and J. Jones, "SMTP MTA Strict Transport Security (MTA-
              STS)", RFC 8461, DOI 10.17487/RFC8461, September 2018,
              <https://www.rfc-editor.org/rfc/rfc8461>.

   [RFC8601]  Kucherawy, M., "Message Header Field for Indicating
              Message Authentication Status", RFC 8601,
              DOI 10.17487/RFC8601, May 2019,
              <https://www.rfc-editor.org/rfc/rfc8601>.

   [RFC8615]  Nottingham, M., "Well-Known Uniform Resource Identifiers
              (URIs)", RFC 8615, DOI 10.17487/RFC8615, May 2019,
              <https://www.rfc-editor.org/rfc/rfc8615>.

   [RFC8620]  Jenkins, N. and C. Newman, "The JSON Meta Application
              Protocol (JMAP)", RFC 8620, DOI 10.17487/RFC8620, July
              2019, <https://www.rfc-editor.org/rfc/rfc8620>.

   [RFC8621]  Jenkins, N. and C. Newman, "The JSON Meta Application
              Protocol (JMAP) for Mail", RFC 8621, DOI 10.17487/RFC8621,
              August 2019, <https://www.rfc-editor.org/rfc/rfc8621>.

De Gennaro                 Expires 1 May 2027                  [Page 43]
Internet-Draft                  MTA Hooks                   October 2026

   [RFC8689]  Fenton, J., "SMTP Require TLS Option", RFC 8689,
              DOI 10.17487/RFC8689, November 2019,
              <https://www.rfc-editor.org/rfc/rfc8689>.

   [RFC8949]  Bormann, C. and P. Hoffman, "Concise Binary Object
              Representation (CBOR)", STD 94, RFC 8949,
              DOI 10.17487/RFC8949, December 2020,
              <https://www.rfc-editor.org/rfc/rfc8949>.

   [RFC9110]  Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke,
              Ed., "HTTP Semantics", STD 97, RFC 9110,
              DOI 10.17487/RFC9110, June 2022,
              <https://www.rfc-editor.org/rfc/rfc9110>.

   [RFC9112]  Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke,
              Ed., "HTTP/1.1", STD 99, RFC 9112, DOI 10.17487/RFC9112,
              June 2022, <https://www.rfc-editor.org/rfc/rfc9112>.

   [RFC9113]  Thomson, M., Ed. and C. Benfield, Ed., "HTTP/2", RFC 9113,
              DOI 10.17487/RFC9113, June 2022,
              <https://www.rfc-editor.org/rfc/rfc9113>.

   [RFC9325]  Sheffer, Y., Saint-Andre, P., and T. Fossati,
              "Recommendations for Secure Use of Transport Layer
              Security (TLS) and Datagram Transport Layer Security
              (DTLS)", BCP 195, RFC 9325, DOI 10.17487/RFC9325, November
              2022, <https://www.rfc-editor.org/rfc/rfc9325>.

   [RFC9457]  Nottingham, M., Wilde, E., and S. Dalal, "Problem Details
              for HTTP APIs", RFC 9457, DOI 10.17487/RFC9457, July 2023,
              <https://www.rfc-editor.org/rfc/rfc9457>.

   [RFC9846]  Rescorla, E., "The Transport Layer Security (TLS) Protocol
              Version 1.3", RFC 9846, DOI 10.17487/RFC9846, July 2026,
              <https://www.rfc-editor.org/rfc/rfc9846>.

   [X.690]    ITU-T, "Information technology - ASN.1 encoding rules:
              Specification of Basic Encoding Rules (BER), Canonical
              Encoding Rules (CER) and Distinguished Encoding Rules
              (DER)", ITU-T Recommendation X.690, 2021.

12.2.  Informative References

De Gennaro                 Expires 1 May 2027                  [Page 44]
Internet-Draft                  MTA Hooks                   October 2026

   [I-D.gondwana-dkim2-authres]
              Gondwana, B., "Reporting DKIM2 Verification Results in
              Authentication-Results", Work in Progress, Internet-Draft,
              draft-gondwana-dkim2-authres-01, 28 October 2026,
              <https://datatracker.ietf.org/doc/html/draft-gondwana-
              dkim2-authres-01>.

   [I-D.ietf-dkim-dkim2-spec]
              Clayton, R., Chuang, W., and B. Gondwana, "DomainKeys
              Identified Mail Signatures v2 (DKIM2)", Work in Progress,
              Internet-Draft, draft-ietf-dkim-dkim2-spec-08, 27 October
              2026, <https://datatracker.ietf.org/doc/html/draft-ietf-
              dkim-dkim2-spec-08>.

   [MILTER]   Proofpoint, Inc., "Sendmail Mail Filter API (Milter)",
              n.d., <https://www.sendmail.org/doc/sendmail-
              current/libmilter/docs/>.

   [PROXY]    Tarreau, W., "The PROXY protocol, Versions 1 and 2", n.d.,
              <https://www.haproxy.org/download/1.8/doc/proxy-
              protocol.txt>.

   [RFC6376]  Crocker, D., Ed., Hansen, T., Ed., and M. Kucherawy, Ed.,
              "DomainKeys Identified Mail (DKIM) Signatures", STD 76,
              RFC 6376, DOI 10.17487/RFC6376, September 2011,
              <https://www.rfc-editor.org/rfc/rfc6376>.

   [RFC7208]  Kitterman, S., "Sender Policy Framework (SPF) for
              Authorizing Use of Domains in Email, Version 1", RFC 7208,
              DOI 10.17487/RFC7208, April 2014,
              <https://www.rfc-editor.org/rfc/rfc7208>.

   [RFC7942]  Sheffer, Y. and A. Farrel, "Improving Awareness of Running
              Code: The Implementation Status Section", BCP 205,
              RFC 7942, DOI 10.17487/RFC7942, July 2016,
              <https://www.rfc-editor.org/rfc/rfc7942>.

   [RFC8314]  Moore, K. and C. Newman, "Cleartext Considered Obsolete:
              Use of Transport Layer Security (TLS) for Email Submission
              and Access", RFC 8314, DOI 10.17487/RFC8314, January 2018,
              <https://www.rfc-editor.org/rfc/rfc8314>.

   [RFC9114]  Bishop, M., Ed., "HTTP/3", RFC 9114, DOI 10.17487/RFC9114,
              June 2022, <https://www.rfc-editor.org/rfc/rfc9114>.

   [RFC9562]  Davis, K., Peabody, B., and P. Leach, "Universally Unique
              IDentifiers (UUIDs)", RFC 9562, DOI 10.17487/RFC9562, May
              2024, <https://www.rfc-editor.org/rfc/rfc9562>.

De Gennaro                 Expires 1 May 2027                  [Page 45]
Internet-Draft                  MTA Hooks                   October 2026

   [RFC9989]  Herr, T., Ed. and J. Levine, Ed., "Domain-Based Message
              Authentication, Reporting, and Conformance (DMARC)",
              RFC 9989, DOI 10.17487/RFC9989, May 2026,
              <https://www.rfc-editor.org/rfc/rfc9989>.

   [XCLIENT]  The Postfix Project, "Postfix XCLIENT Howto", n.d.,
              <https://www.postfix.org/XCLIENT_README.html>.

   [XFORWARD] The Postfix Project, "Postfix XFORWARD Howto", n.d.,
              <https://www.postfix.org/XFORWARD_README.html>.

Appendix A.  Error Codes

   Error responses are Problem Details [RFC9457] with the media type
   "application/problem+json", whatever serialization the registration
   uses, and an extension member "code" holding one of the codes below:

   {
     "type": "about:blank",
     "status": 404,
     "title": "Not Found",
     "code": "REGISTRATION_NOT_FOUND",
     "detail": "No registration reg-7a3b9c2e"
   }

    +========+============================+===========================+
    | Status | Code                       | Meaning                   |
    +========+============================+===========================+
    | 400    | INVALID_REQUEST            | Malformed body or missing |
    |        |                            | required fields           |
    +--------+----------------------------+---------------------------+
    | 400    | INVALID_FILTER             | Filter is syntactically   |
    |        |                            | invalid                   |
    +--------+----------------------------+---------------------------+
    | 400    | MISSING_REQUEST_ID         | MTA-Hooks-Request-Id is   |
    |        |                            | missing                   |
    +--------+----------------------------+---------------------------+
    | 401    | AUTHENTICATION_REQUIRED    | No credentials were       |
    |        |                            | provided                  |
    +--------+----------------------------+---------------------------+
    | 401    | INVALID_CREDENTIALS        | Credentials are invalid   |
    |        |                            | or expired                |
    +--------+----------------------------+---------------------------+
    | 403    | REGISTRATION_DENIED        | The MTA may not register  |
    +--------+----------------------------+---------------------------+
    | 403    | REGISTRATION_MISMATCH      | Credentials are not bound |
    |        |                            | to the registration       |
    +--------+----------------------------+---------------------------+

De Gennaro                 Expires 1 May 2027                  [Page 46]
Internet-Draft                  MTA Hooks                   October 2026

    | 404    | REGISTRATION_NOT_FOUND     | No such registration      |
    +--------+----------------------------+---------------------------+
    | 409    | REGISTRATION_LIMIT_REACHED | Too many registrations    |
    |        |                            | for this MTA              |
    +--------+----------------------------+---------------------------+
    | 413    | REQUEST_TOO_LARGE          | Request body larger than  |
    |        |                            | the scanner accepts       |
    +--------+----------------------------+---------------------------+
    | 422    | UNSUPPORTED_SERIALIZATION  | The serialization is not  |
    |        |                            | supported                 |
    +--------+----------------------------+---------------------------+
    | 422    | UNSUPPORTED_STAGE          | A stage is not supported  |
    +--------+----------------------------+---------------------------+
    | 422    | UNSUPPORTED_PROPERTY       | A property is not         |
    |        |                            | supported                 |
    +--------+----------------------------+---------------------------+
    | 422    | UNSUPPORTED_FILTER         | A filter condition is not |
    |        |                            | supported                 |
    +--------+----------------------------+---------------------------+
    | 429    | RATE_LIMITED               | Too many requests         |
    +--------+----------------------------+---------------------------+
    | 500    | INTERNAL_ERROR             | Unexpected error          |
    +--------+----------------------------+---------------------------+
    | 503    | UNAVAILABLE                | Temporarily unable to     |
    |        |                            | process requests          |
    +--------+----------------------------+---------------------------+

                            Table 9: Error Codes

Appendix B.  Acknowledgments

   The author thanks the operators of mailbox.org for their detailed
   proposals on routing, observability, and inbound signals, and the
   developers and operators of existing mail filtering systems whose
   experience informed this design.

Appendix C.  Changes

   [[This section to be removed by RFC Editor]]

   *draft-degennaro-mta-hooks-02*

   *  Responses are JSON Patch documents (RFC 6902), applied atomically;
      the custom set/add/delete format and its ordering rules are
      removed.

De Gennaro                 Expires 1 May 2027                  [Page 47]
Internet-Draft                  MTA Hooks                   October 2026

   *  Registrations consist of ordered subscriptions, each with its own
      filter, properties, grants, and failure policy.  Filters gain
      connection conditions.

   *  Replaced updateProperties with grants that can restrict values and
      sizes.  Ordered properties can only become stricter unless a grant
      relaxes them, which replaces the downgrade rule.  The discovery
      document lists the changes a scanner wants as grants, replacing
      its actions and updates lists.

   *  Actions apply to inbound stages only.  Outbound outcomes,
      including DSN suppression, are expressed through recipient status,
      replacing "continue" and "cancel".  Removed the "defer" stage,
      which duplicated "delivery".

   *  At the "route" and "delivery" stages subscriptions are matched per
      recipient, so grants and failure handling apply only to the
      recipients a subscription matches.

   *  Added the "route" stage with per-recipient routes, and the
      "transmit" stage, which runs per SMTP transaction so scanners can
      sign each copy with its final envelope, as DKIM2 requires.

   *  Removed ARC, which the DMARC working group proposes to move to
      Historic, from sender authentication and added DKIM2.

   *  Bearer tokens (RFC 6750) are mandatory to implement.  Error
      responses use Problem Details (RFC 9457).  Header field values
      have a defined syntax.

   *  Serializations are named by media type; removed the serialization
      registry.  The registration request carries the registration's
      label, and no longer carries metadata.  A scanner can only narrow
      a registration by removing properties and grants.

   *  Certificates report a verification failure using RFC 8460 result
      types.  Sessions also describe non-SMTP submissions.

   *  One Recipient object across all stages with a common status set
      and a decidedBy attribution; rejected recipients remain in the
      array; "failed-silent" became "discarded".

   *  Properties are no longer split by direction.  Client, TLS, and
      authentication data are grouped into a Session object, which is
      available at outbound stages when the MTA knows it.

De Gennaro                 Expires 1 May 2027                  [Page 48]
Internet-Draft                  MTA Hooks                   October 2026

   *  Added session identifier and counters, proxy information, EHLO
      capabilities, certificate chains with verification result, per-
      attempt remote host and TLS details using RFC 8460 values,
      delayMs, a per-request timeoutMs, traceId, and extension data with
      message lifetime.

   *  Clarified ESMTP parameters and queue identifier stability.  Added
      iprev to sender authentication.

   *  Dropped the "X-" prefix from header fields (RFC 6648) and added
      MTA-Hooks-Context.

   *  Simplified the registration lifecycle to GET and DELETE on the
      registration URL; removed the status endpoint, its statistics, the
      registration status and creation time, and the maxRegistrations
      limit.

   *draft-degennaro-mta-hooks-01*

   *  Inverted the registration model: the MTA is now exclusively the
      HTTP client.

   *  Required a registration identifier and request identifier on every
      hook request.

   *  Specified authentication, error responses, and action precedence.

   *draft-degennaro-mta-hooks-00*

   *  Initial version

Author's Address

   Mauro De Gennaro
   Stalwart Labs LLC
   1309 Coffeen Avenue, Suite 1200
   Sheridan, WY 82801
   United States of America
   Email: mauro@stalw.art
   URI:   https://stalw.art

De Gennaro                 Expires 1 May 2027                  [Page 49]