Skip to content

Instantly share code, notes, and snippets.

@mikaelhg
Created June 9, 2025 17:09
Show Gist options
  • Save mikaelhg/1c25d02e4060e1bee02159791d433fb0 to your computer and use it in GitHub Desktop.
Save mikaelhg/1c25d02e4060e1bee02159791d433fb0 to your computer and use it in GitHub Desktop.
Milter 6 specification

This document specifies the Sendmail Milter protocol, version 6, integrating the version 2 base protocol with subsequent extensions.

1. General

1.1. Notation

All integer types are transmitted in network byte order (big-endian). Data is byte-aligned.

  • uint32: 32-bit unsigned integer.
  • uint16: 16-bit unsigned integer.
  • char: 8-bit character/byte.
  • char name[]: A NUL-terminated string.
  • char buf[len]: A fixed-length character array of len bytes, not NUL-terminated.
  • char args[][]: An array of NUL-terminated strings, concatenated one after another, filling the data portion of the packet.

1.2. Link/Packet Protocol

Communication occurs over a stream-based socket (TCP or Unix). Data is sent in packets with the following structure:

  • uint32 len: Total length of the packet's payload (cmd + data).
  • char cmd: A single-character command or response code.
  • char data[len-1]: Code-specific data.

The MTA initiates and drives the conversation. The filter MUST only send a response when one is expected for a given command.

2. Protocol Negotiation (SMFIC_OPTNEG)

The first command sent by the MTA after connection is SMFIC_OPTNEG. The filter must respond with its own SMFIC_OPTNEG packet to establish the parameters for the session.

MTA to Filter Packet ('O'):

  • uint32 version: Protocol version offered by MTA (e.g., 6).
  • uint32 actions: Bitmask of actions the MTA supports (SMFIF_* flags).
  • uint32 protocol: Bitmask of protocol steps the MTA can provide (SMFIP_* flags).

Filter to MTA Packet ('O'):

  • uint32 version: Protocol version selected by filter (must be <= MTA version).
  • uint32 actions: Bitmask of actions the filter may perform. This MUST be a subset of the actions offered by the MTA.
  • uint32 protocol: Bitmask of protocol steps the filter wishes to skip.

2.1. Action Flags (SMFIF_*)

Set by the filter to declare which modification actions it may use.

Flag Value Action
ADDHDRS 0x000001 Add or insert headers (SMFIR_ADDHEADER, SMFIR_INSHEADER).
CHGBODY 0x000002 Replace the message body (SMFIR_REPLBODY).
ADDRCPT 0x000004 Add a recipient (SMFIR_ADDRCPT).
DELRCPT 0x000008 Delete a recipient (SMFIR_DELRCPT).
CHGHDRS 0x000010 Change or delete a header (SMFIR_CHGHEADER).
QUARANTINE 0x000020 Quarantine the message (SMFIR_QUARANTINE).
CHGFROM 0x000040 Change the envelope sender (SMFIR_CHGFROM).
ADDRCPT_PAR 0x000080 Add a recipient with ESMTP parameters (SMFIR_ADDRCPT_PAR).
SETSYMLIST 0x000100 Declare which macros the filter requires (SMFIR_SETSYMLIST).

2.2. Protocol Flags (SMFIP_*)

Set by the filter to request that the MTA skip certain commands or alter its behavior.

Flag Value Behavior
Skip Commands
NOCONNECT 0x000001 Skip SMFIC_CONNECT.
NOHELO 0x000002 Skip SMFIC_HELO.
NOMAIL 0x000004 Skip SMFIC_MAIL.
NORCPT 0x000008 Skip SMFIC_RCPT.
NOBODY 0x000010 Skip all SMFIC_BODY chunks.
NOHDRS 0x000020 Skip all SMFIC_HEADER commands.
NOEOH 0x000040 Skip SMFIC_EOH.
NOUNKNOWN 0x000100 Do not send unknown SMTP commands (SMFIC_UNKNOWN).
NODATA 0x000200 Do not send SMFIC_DATA.
Suppress Replies
NR_CONN 0x00010000 Do not expect a reply for SMFIC_CONNECT.
NR_HELO 0x00020000 Do not expect a reply for SMFIC_HELO.
NR_MAIL 0x00040000 Do not expect a reply for SMFIC_MAIL.
NR_RCPT 0x00080000 Do not expect a reply for SMFIC_RCPT.
NR_DATA 0x00100000 Do not expect a reply for SMFIC_DATA.
NR_UNKN 0x00200000 Do not expect a reply for SMFIC_UNKNOWN.
NR_HDR 0x00000080 Do not expect a reply for SMFIC_HEADER.
NR_EOH 0x00400000 Do not expect a reply for SMFIC_EOH.
NR_BODY 0x00800000 Do not expect a reply for SMFIC_BODY.
Other Behavior
SKIP 0x00000400 Filter can use SMFIR_SKIP to stop receiving body chunks early.
RCPT_REJ 0x00000800 Send SMFIC_RCPT for recipients that were already rejected by the MTA.
HDR_LEADSPC 0x01000000 Send header values with leading space intact. MTA will not auto-add spaces.
MDS_256K 0x10000000 Max DATA size is 256K (semantics unconfirmed).
MDS_1M 0x20000000 Max DATA size is 1M (semantics unconfirmed).

3. Command Codes (MTA to Filter)

Code Name Data Structure Response Expected?
'A' ABORT (none) No
'B' BODY char buf[] (up to 65535 bytes of a body chunk) Yes
'C' CONNECT char hostname[], char family, [uint16 port], char address[] Yes
'D' MACRO char cmdcode, char nameval[][] (pairs of macro name and value) No
'E' BODYEOB (none) End of Body. Yes (Mod + A/R)
'H' HELO char helo[] (HELO/EHLO string) Yes
'L' HEADER char name[], char value[] Yes
'M' MAIL char args[][] (MAIL FROM address and ESMTP args) Yes
'N' EOH (none) End of Headers. Yes
'O' OPTNEG uint32 version, uint32 actions, uint32 protocol Yes (SMFIC_OPTNEG)
'Q' QUIT (none) No (Close conn)
'R' RCPT char args[][] (RCPT TO address and ESMTP args) Yes
'T' DATA (none) Sent upon receiving the DATA command. Yes
'U' UNKNOWN char command[] (The unknown SMTP command string) Yes

4. Response Codes (Filter to MTA)

4.1. Accept/Reject Actions

Sent in response to commands that expect a reply. A conversation ends with one of these.

Code Name Data Structure Description
'a' ACCEPT (none) Accept message and stop processing this filter.
'c' CONTINUE (none) Accept this stage and continue processing.
'd' DISCARD (none) Silently discard the message.
'r' REJECT (none) Reject with a permanent (5xx) error.
't' TEMPFAIL (none) Reject with a temporary (4xx) error.
'y' REPLYCODE char smtpcode[3], char ' ', char text[] Reject with a specific [45]xx code and message.
's' SKIP (none) Stop receiving SMFIC_BODY chunks. Proceed to SMFIC_BODYEOB.

4.2. Modification Actions

Sent in response to SMFIC_BODYEOB, just before a final accept/reject action.

Code Name Data Structure Description
'+' ADDRCPT char rcpt[] Add a recipient to the envelope.
'-' DELRCPT char rcpt[] Remove a recipient from the envelope.
'2' ADDRCPT_PAR char args[][] (recipient and ESMTP args) Add a recipient with ESMTP parameters.
'b' REPLBODY char buf[] Replace a chunk of the message body.
'e' CHGFROM char args[][] (sender and ESMTP args) Change the envelope sender address.
'h' ADDHEADER char name[], char value[] Add a header to the end of the header block.
'i' INSHEADER uint32 index, char name[], char value[] Insert a header at the specified index.
'm' CHGHEADER uint32 index, char name[], char value[] Change header at index. Empty value deletes it.
'q' QUARANTINE char reason[] Quarantine the message with a given reason.

4.3. Other Responses

Code Name Data Structure Description
'p' PROGRESS (none) Sent asynchronously to reset MTA timeout during a long operation.
'l' SETSYMLIST char cmdcode, char macros[][] Declare required macros for a given stage. (Semantics not fully specified).

5. Conversation Flow

A single milter connection can handle multiple email transactions. The state resets after each completed message.

5.1. Session Initialization

  1. Connection: The MTA establishes a stream connection to the filter.
  2. Negotiation: The MTA sends a single SMFIC_OPTNEG ('O') command, declaring its capabilities (version, supported actions, available protocol steps).
  3. Filter Response: The filter MUST reply with its own SMFIC_OPTNEG ('O') packet, selecting a compatible version and declaring the actions it will perform and the protocol steps it wishes to skip. This negotiation governs the entire life of the connection.

5.2. Transaction Start

For each new SMTP session, the following sequence occurs, unless skipped by negotiation (SMFIP_NOCONNECT, SMFIP_NOHELO).

  1. (Optional) Connection Info: The MTA sends SMFIC_MACRO ('D') for the connect stage, followed by SMFIC_CONNECT ('C'). The filter replies with an Accept/Reject (A/R) action. A reject terminates the SMTP session.
  2. (Optional) HELO/EHLO: The MTA sends SMFIC_MACRO ('D') for the HELO stage, followed by SMFIC_HELO ('H'). The filter replies with an A/R action.

5.3. Message Processing

For each message within the SMTP session, the following sequence occurs, unless stages are skipped by negotiation.

  1. Mail From: The MTA sends SMFIC_MACRO ('D') for the mail stage, followed by SMFIC_MAIL ('M'). The filter replies with an A/R action.
  2. RCPT To: For each recipient, the MTA sends SMFIC_MACRO ('D') for the recipient stage, followed by SMFIC_RCPT ('R'). The filter replies with an A/R action for each recipient.
  3. Headers: For each header, the MTA sends SMFIC_HEADER ('L'). The filter replies with an A/R action (unless SMFIP_NR_HDR is set).
  4. End of Headers: The MTA sends SMFIC_EOH ('N'). The filter replies with an A/R action.
  5. Body: The MTA sends the message body in one or more SMFIC_BODY ('B') chunks. The filter replies with an A/R action for each chunk (unless SMFIP_NR_BODY is set).
    • The filter may respond with SMFIR_SKIP ('s') at any point during this stage to request that the MTA stop sending body chunks and proceed immediately to the End of Body stage.

5.4. Message Conclusion and Modification

  1. End of Body: The MTA sends SMFIC_BODYEOB ('E').
  2. Filter Response: The filter MUST reply with a sequence of packets: a. Zero or more Modification Action packets (e.g., SMFIR_ADDHEADER, SMFIR_CHGFROM, SMFIR_REPLBODY). b. Exactly one final Accept/Reject Action packet (e.g., SMFIR_ACCEPT, SMFIR_REJECT, SMFIR_CONTINUE).

5.5. State Reset and Termination

  • SMFIR_ACCEPT / SMFIR_CONTINUE: After a message is accepted, the filter's state resets to the post-HELO stage, ready to process a new SMFIC_MAIL command on the same connection.
  • SMFIC_ABORT ('A'): The MTA signals an unexpected end to the current message transaction (e.g., client RSET). The filter MUST discard state for the current message and reset to the post-HELO stage. No response is sent.
  • SMFIC_QUIT ('Q'): The MTA signals the end of the SMTP session. The filter MUST close the connection. No response is sent.

6. Detailed Command and Response Semantics

6.1. Macro Transmission (SMFIC_MACRO)

  • SMFIC_MACRO ('D') packets provide context for the SMTP command that immediately follows them.
  • The cmdcode field in the packet indicates which command the macros apply to (e.g., 'C' for CONNECT, 'M' for MAIL).
  • Filters SHOULD be prepared to receive a SMFIC_MACRO packet before any command that can have associated macros.

6.2. Header Modification (SMFIR_CHGHEADER, SMFIR_INSHEADER)

  • SMFIR_CHGHEADER ('m'): The index is 1-based and scoped per header name. An index of 2 for name "Received" targets the second "Received" header. A zero-length string for value (a single NUL byte) deletes the specified header.
  • SMFIR_INSHEADER ('i'): The index is 1-based and specifies the absolute position in the header list where the new header will be inserted. An index of 1 inserts at the top.

6.3. Body Replacement (SMFIR_REPLBODY)

  • Use of SMFIR_REPLBODY ('b') implies replacement of the entire message body. The original body is discarded.
  • The filter must supply a new, complete body. This new body can be split across multiple SMFIR_REPLBODY packets.
  • The sequence of SMFIR_REPLBODY packets is sent during the SMFIC_BODYEOB stage and is terminated by a final Accept/Reject action.

6.4. Asynchronous Progress (SMFIR_PROGRESS)

  • The filter can send a SMFIR_PROGRESS ('p') packet at any time it is performing a long-running task to prevent the MTA from timing out.
  • The MTA will consume the progress packet and reset its response timer, continuing to wait for the definitive response.

7. Further Command Clarifications

  • SMFIC_DATA ('T'): Sent by the MTA upon receiving the client's DATA command, occurring after all RCPT commands but before any HEADER commands. This provides an early stage to reject a message before header or body content is transmitted.

  • SMFIC_UNKNOWN ('U'): The MTA forwards an SMTP command string that it does not recognize. This allows the filter to implement custom or non-standard commands. The filter can respond with any A/R action.

  • SMFIR_CONN_FAIL ('f'): This response instructs the MTA to terminate the client SMTP session immediately, typically with a 421 "Service not available" error. It differs from REJECT or TEMPFAIL, which apply to a specific SMTP command, whereas CONN_FAIL applies to the entire connection. The specification for this response is incomplete.

  • SMFIC_QUIC_NC ('K'): "QUIT but new connection follows". This is an advisory command from the MTA indicating it is closing the current milter connection but intends to establish a new one immediately. It is an optimization hint. The filter should treat it identically to SMFIC_QUIT ('Q'). The specification for this command is incomplete.

  • SMFIR_SETSYMLIST ('l'): Used by the filter to declare the specific macros it requires for each protocol stage. This is a response, typically sent once after OPTNEG, to reduce unnecessary data transmission from the MTA. The specification for its usage is incomplete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment