Skip to content

Instantly share code, notes, and snippets.

@RavuAlHemio
Last active July 8, 2025 23:42
Show Gist options
  • Save RavuAlHemio/74e5403fd5d487f03ad0cabb4b4dec04 to your computer and use it in GitHub Desktop.
Save RavuAlHemio/74e5403fd5d487f03ad0cabb4b4dec04 to your computer and use it in GitHub Desktop.
Milter protocol

Milter protocol

Milters are a quasi-standardized protocol to allow a Mail Transfer Agent (MTA) to delegate modifying, as well as making decisions about, e-mail messages to an external program.

The milter protocol was implemented as part of Sendmail:

protocol version Sendmail version
1 8.10
2 8.11
3 8.13
4 8.13
4.1 8.13.5
6 8.14

It eventually also found its way into Postfix, starting with version 2.3.

The MTA connects to the milter via a stream socket -- Unix, IPv4 or IPv6. However, this stream can be subdivided into individual packets. The packets sent between the MTA and the milter have the following structure:

struct MilterPacket {
    length: i32be, // comprises the length of `command` and `data`
    command: u8,
    data: [u8; length - 1],
}

length must be at least 1. In protocol version 1, length cannot be greater than 65536; since version 2, that limitation is lifted.

Conventions

A NulTerminatedString is a sequence of bytes that continues up to and including a terminating zero (0x00) byte. The single-L spelling refers to the character NUL (U+0000) as opposed to a NULL pointer.

A RestOfPacketString may only occur at the end of a packet's data; it is a sequence of bytes that continues up to the end of the packet data (as announced by the length field in each packet).

Hexadecimal numbers are specified with the 0x prefix; digits ten through fifteen are represented by the letters A through F. For example, 0x2A corresponds to 42.

The = character is encoded as 0x3D.

Communication

This process illustrates the general flow between a client, the MTA and the milter. It is assumed that the MTA supports a sufficiently current version of the milter protocol (otherwise some events might not trigger a request-response sequence) and the milter has neither opted out of specific events (using the IGNORE_* flags) nor announced that it will not respond to specific events (using the NO_*_RESPONSE flags).

  1. Right after connecting, the MTA sends a NegotiationRequest to the milter.

  2. The milter replies with a NegotiationResponse in return.

  3. A client connects to the MTA.

  4. The MTA sends a ConnectRequest to the milter. The milter responds with one of the decision responses (except ReplyCodeResponse, because no custom codes are allowed at this stage.)

  5. The client sends a HELO or EHLO command to the MTA.

  6. The MTA sends a HeloRequest to the milter. The milter responds with one of the decision responses.

  7. The client sends a MAIL FROM command to the MTA.

  8. The MTA sends a MailFromRequest to the milter. The milter responds with one of the decision responses.

  9. The client sends a RCPT TO command to the MTA.

  10. The MTA sends a RcptToRequest to the milter. The milter responds with one of the decision responses.

  11. Steps 9 and 10 are repeated as appropriate for any additional recipient.

  12. The client sends a DATA command to the MTA.

  13. The MTA sends a DataRequest to the milter. The milter responds with one of the decision responses.

  14. The client starts supplying the content of the e-mail message, consisting of headers followed by the body.

  15. For every header supplied by the client, the MTA sends a HeaderRequest to the milter. The milter responds with one of the decision responses.

  16. Once the client terminates the headers with the CR LF CR LF sequence, the MTA sends an EndOfHeadersRequest. The milter responds with one of the decision responses.

  17. As the client supplies more and more of the body, the MTA transmits it using BodyRequests to the milter. The milter always responds with one of the decision responses.

  18. Once the client is finished transmitting the body (CR LF . CR LF), the MTA transmits an EndOfBodyRequest to the milter.

  19. The milter may now (finally) make changes to the message by sending as many of the mutating responses as required.

  20. Finally, the milter sends one of the decision responses (except ContinueResponse and SkipResponse) to voice its ultimate opinion about the message. It should now reset any message-specific state.

  21. If the client remains connected and wishes to send another e-mail, the whole process repeats from step 7.

  22. When the client quits/disconnects, the MTA either sends a QuitRequest to the milter and immediately closes the connection to it, whereupon this process ends. Alternatively, it sends a QuitNewRequest to the milter, whereupon this process repeats from step 3.

The MTA may also send an AbortRequest, which means that the current message is no longer being processed. The milter should then reset any message-specific state and continue from step 7.

Packet types

The format of the data field of each MilterPacket depends on the command.

structure name command direction data length since version response types description
NegotiationRequest O MTA → milter 8/12 1 O initial packet, capability negotiation
NegotiationResponse O milter → MTA 8/12 1 =O capability negotiation
SetMacrosResponse l milter → MTA n/a 6 - only send subset of macros
AbortRequest A MTA → milter 0 1 - current message abandoned
QuitRequest Q MTA → milter 0 1 - client connection closed
QuitNewRequest K MTA → milter 0 6 - client connection closed but a new one will follow
DefineMacroRequest D MTA → milter variable 1 - macro keys and values for a future request
ConnectRequest C MTA → milter variable 1 C D new client connected
HeloRequest H MTA → milter variable 1 C D R client issued HELO/EHLO
MailFromRequest M MTA → milter variable 1 C D R client issued MAIL FROM
RcptToRequest R MTA → milter variable 1 C D R client issued RCPT TO
DataRequest T MTA → milter 0 4 C D R client issued DATA
UnknownRequest U MTA → milter variable 3 C D R client issued a non-standard command
HeaderRequest L MTA → milter variable 1 C D R client passed a header as part of a mail's DATA
EndOfHeadersRequest N MTA → milter 0 2 C D R no more headers as part of a mail's DATA
BodyRequest B MTA → milter variable 1 C D R chunk of mail's body passed in DATA
EndOfBodyRequest E MTA → milter variable/0 1 D M R last chunk of mail's body passed in DATA, now the fun starts
AddHeaderResponse h milter → MTA variable 1 =M add header to message
InsertHeaderResponse i milter → MTA variable 3 =M insert header at specific location in message
ModifyHeaderResponse m milter → MTA variable 2 =M change or delete existing header in message
ChangeSenderResponse e milter → MTA variable 6 =M change message sender
AddRecipientResponse + milter → MTA variable 1 =M add recipient to message
AddRecipientArgsResponse 2 milter → MTA variable 6 =M add recipient to message including ESMTP args
DeleteRecipientResponse - milter → MTA variable 1 =M remove recipient from message
ProgressResponse p milter → MTA 0 1 =M wait, I'm not done yet
ReplaceBodyResponse b milter → MTA variable 1 =M replace body
AcceptResponse a milter → MTA 0 1 =D final decision: deliver it
ContinueResponse c milter → MTA 0 1 =C go ahead to the next stage
SkipResponse s milter → MTA 0 6 =C go ahead to the next stage but skip some requests (depending on MTA)
DiscardResponse d milter → MTA 0 1 =D final decision: pretend to deliver it but don't
QuarantineResponse q milter → MTA 0 3 =D final decision: quarantine it
RejectResponse r milter → MTA 0 1 =D final decision: reject it with a 5xx
ReplyCodeResponse y milter → MTA variable 1 =R final decision: reject it with a custom 4xx/5xx message
TempFailResponse t milter → MTA 0 1 =D final decision: reject it with a 4xx
ShutdownResponse 4 milter → MTA 0 3 =D final decision: cut off the connection
ConnFailureResponse f milter → MTA n/a 6 =D cause a connection failure

The "response types" column lists the classes of responses expected to a given request. For example, "O" means that a response of class O is expected, while "=O" means that this is a response of class O. A single hyphen means that the MTA does not expect a response to this request.

NegotiationRequest

The NegotiationRequest is the first packet transmitted between the MTA and the milter after the connection is established; specifically, it is transmitted from the MTA to the milter, which is then expected to respond with a NegotiationResponse.

#[command = 'O']
struct NegotiationRequest {
    version: u32be, // protocol version supported by MTA
    if version == 1 {
        flags: u32be, // always 0
    } else {
        supported_filter_flags: u32be,
        supported_protocol_flags: u32be,
    }
}

Minor revisions such as 4.1 are advertised as the major version (4) with additional supported flags.

NegotiationResponse

The NegotiationResponse is transmitted from the milter to the MTA after the MTA's NegotiationRequest greeting.

#[command = 'O']
struct NegotiationResponse {
    version: u32be, // must match server's version number
    if version == 1 {
        flags: u32be,
    } else {
        filter_flags: u32be,
        protocol_flags: u32be,
    }
}

flags (version 1) or filter_flags and protocol_flags (version 2 and above) are bitwise OR values of flags documented in their own section. Since version 2, the milter must not specify any filter or protocol flags that the server does not support according to the NegotiationRequest.

SetMacrosResponse

The SetMacrosResponse is never actually transmitted from the milter to the MTA; the MTA always sends all macros and the filtering is done by the milter itself (within libmilter).

The command character l is reserved since version 6 for a potential future MTA-side implementation.

AbortRequest

An AbortRequest is sent by the MTA to inform the milter that the current message has been abandoned. (However, the client's connection is still open and it might send a new message.) The MTA does not expect a response to this request.

#[command = 'A']
struct AbortRequest {
}

QuitRequest

A QuitRequest is sent by the MTA to inform the milter that the client has disconnected. The MTA then closes the connection to the milter without waiting for a response.

#[command = 'Q']
struct QuitRequest {
}

QuitNewRequest

A QuitNewRequest is sent by the MTA to inform the milter that the client has disconnected but a new client connection will follow. The milter is expected not to send a response.

#[command = 'K']
#[version >= 6]
struct QuitNewRequest {
}

DefineMacroRequest

A DefineMacroRequest is sent by the MTA to define macro values which the milter can use as an additional source of metadata. The MTA does not expect a response to this request.

#[command = 'D']
struct DefineMacroRequest {
    for_command: u8,
    zero_or_more {
        macro_name: NulTerminatedString,
        macro_value: NulTerminatedString,
    },
}

for_command contains the byte representing the command (generally an upcoming request command) for which these macro values are relevant.

ConnectRequest

A ConnectRequest is sent by the MTA to the milter when a new client connects to the MTA.

#[command = 'C']
struct ConnectRequest {
    hostname: NulTerminatedString,
    family: u8, // 'U'|'L'|'4'|'6'
    if family != 'U' {
        port: u16,
        if version == 1 {
            socket_info: RestOfPacketString,
        } else {
            socket_info: NulTerminatedString,
        }
    }
}

The hostname is the hostname of the connecting client, as resolved by the system or MTA.

The values for family are the following:

character hex description port socket info
U 0x55 unknown socket type not in packet not in packet
L 0x4C local/Unix socket always 0 socket path
4 0x34 IPv4 socket TCP port client's IP address as string
6 0x36 IPv6 socket TCP port client's IP address as string

Note that an IP connection may have been established using a different stream protocol than TCP; in such a case, TCP port may contain the value of a similar parameter of that protocol, such as the SCTP port number.

This request is not sent if the milter specified the IGNORE_CONNECT flag flag in its NegotiationResponse.

If the protocol flag NO_CONNECT_RESPONSE (version 6 or above) is specified, the milter is expected not to respond to ConnectRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ShutdownResponse (since version 3)
  • TempFailResponse

The milter should not respond with any other response, including ReplyCodeResponse (as SMTP does not allow for custom error codes in the greeting).

HeloRequest

A HeloRequest is sent by the MTA to the milter when the client sends a HELO or EHLO command to the MTA.

#[command = 'H']
struct HeloRequest {
    parameter: NulTerminatedString,
}

parameter is the parameter passed to the HELO or EHLO command (generally the client's hostname). Note that a milter cannot differentiate between HELO and EHLO.

This request is not sent if the milter specified the IGNORE_HELO flag flag in its NegotiationResponse.

If the protocol flag NO_HELO_RESPONSE (version 6 or above) is specified, the milter is expected not to respond to HeloRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

The milter should not respond with any other response.

MailFromRequest

A MailFromRequest is sent by the MTA to the milter when the client sends a MAIL FROM command to the MTA.

#[command = 'M']
struct MailFromRequest {
    sender: NulTerminatedString,
    zero_or_more {
        mail_parameter: NulTerminatedString,
    },
}

The sender is the specified sender address (including the angled brackets). ESMTP allows for additional parameters to be specified after the sender address, which are transferred as additional NUL-terminated strings. Generally, these additional parameters have a key-value format (separated by the first occurrence of the = character), but this might not be strictly enforced by every MTA.

This request is not sent if the milter specified the IGNORE_MAIL_FROM flag flag in its NegotiationResponse.

If the protocol flag NO_MAIL_FROM_RESPONSE (version 6 or above) is specified, the milter is expected not to respond to MailFromRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

The milter should not respond with any other response.

RcptToRequest

A RcptToRequest is sent by the MTA to the milter when the client sends a RCPT TO command to the MTA.

#[command = 'R']
struct RcptToRequest {
    recipient: NulTerminatedString,
    zero_or_more {
        mail_parameter: NulTerminatedString,
    },
}

The recipient is the specified recipient address (including the angled brackets). ESMTP allows for additional parameters to be specified after the recipient address, which are transferred as additional NUL-terminated strings. Generally, these additional parameters have a key-value format (separated by the first occurrence of the = character), but this might not be strictly enforced by every MTA.

This request is not sent if the milter specified the IGNORE_RCPT_TO flag flag in its NegotiationResponse.

If the protocol flag NO_RCPT_TO_RESPONSE (version 6 or above) is specified, the milter is expected not to respond to RcptToRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

The milter should not respond with any other response.

DataRequest

A DataRequest is sent by the MTA to the milter when the client sends a DATA command to the MTA.

#[command = 'T']
#[version >= 4]
struct DataRequest {
}

This request is not sent if the milter specified the IGNORE_DATA flag flag in its NegotiationResponse.

If the protocol flag NO_DATA_RESPONSE (version 6 or above) is specified, the milter is expected not to respond to DataRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

The milter should not respond with any other response.

UnknownRequest

An UnknownRequest is sent by the MTA to the milter when the client sends some non-standard command to the MTA.

#[command = 'U']
#[version >= 3]
struct UnknownRequest {
    command: NulTerminatedString,
}

This request is not sent if the milter specified the IGNORE_UNKNOWN flag flag in its NegotiationResponse.

If the protocol flag NO_UNKNOWN_RESPONSE (version 6 or above) is specified, the milter is expected not to respond to UnknownRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

The milter should not respond with any other response.

HeaderRequest

A HeaderRequest is sent by the MTA to the milter when the client sends a header as part of the data of an e-mail message.

#[command = 'L']
struct HeaderRequest {
    name: NulTerminatedString,
    value: NulTerminatedString,
}

This request is not sent if the milter specified the IGNORE_HEADERS flag flag in its NegotiationResponse.

If the protocol flag NO_HEADER_RESPONSE (version 3 or above) is specified, the milter is expected not to respond to HeaderRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

The milter should not respond with any other response.

EndOfHeadersRequest

An EndOfHeadersRequest is sent by the MTA to the milter after the final header in the data of an e-mail message has been transmitted.

#[command = 'N']
#[version >= 2]
struct EndOfHeadersRequest {
}

This request is not sent if the milter specified the IGNORE_END_HEADERS flag flag in its NegotiationResponse. It is also not sent by MTAs that only support the version 1 protocol; in that case, the first BodyRequest can be taken as an indirect statement that the headers have ended.

If the protocol flag NO_END_HEADERS_RESPONSE (version 6 or above) is specified, the milter is expected not to respond to EndOfHeadersRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

The milter should not respond with any other response.

BodyRequest

Multiple BodyRequests are sent by the MTA to the milter to represent chunks of the e-mail's body.

#[command = 'B']
struct BodyRequest {
    body_chunk: RestOfPacketString,
}

body_chunk should never be longer than 65535 bytes.

If the protocol flag NO_BODY_RESPONSE (version 6 or above) is specified, the milter is expected not to respond to BodyRequests. Otherwise, the milter is expected to respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • SkipResponse (since version 6)
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

The milter should not respond with any other response.

EndOfBodyRequest

The EndOfBodyRequest is sent by the MTA to the milter after the message body has been transmitted. The milter may then perform modifications on the message body before voicing its opinion on the message's ultimate fate.

#[command = 'E']
struct EndOfBodyRequest {
    if version == 1 {
        body_chunk: RestOfPacketString,
    }
}

In protocol version 1, the request contained the last chunk of the message body. Since protocol version 2, the last chunk of the message body is sent in a BodyRequest and the EndOfBodyRequest contains no data.

This request is sent even if the milter specified the IGNORE_BODY flag flag in its NegotiationResponse.

The milter is expected to respond with zero or more of the following:

  • AddHeaderResponse
  • InsertHeaderResponse (since version 3)
  • ModifyHeaderResponse (since version 2)
  • ChangeSenderResponse (since version 6)
  • AddRecipientResponse
  • AddRecipientArgsResponse (since version 6)
  • DeleteRecipientResponse
  • ProgressResponse
  • QuarantineResponse (since version 3)
  • ReplaceBodyResponse

Finally, the milter must respond with one of the following:

  • AcceptResponse
  • ContinueResponse
  • DiscardResponse
  • RejectResponse
  • ReplyCodeResponse
  • TempFailResponse

AddHeaderResponse

The milter may send zero or more instances of AddHeaderResponse as a response to an EndOfBodyRequest if it wishes to add additional headers to the e-mail message data.

#[command = 'h']
#[filter_flag_gate = ADD_HEADERS]
struct AddHeaderResponse {
    name: NulTerminatedString,
    value: NulTerminatedString,
}

The MTA modifies the message accordingly, then waits for another response from the milter.

InsertHeaderResponse

The milter may send zero or more instances of InsertHeaderResponse as a response to an EndOfBodyRequest if it wishes to insert additional headers at specific locations to the e-mail message data.

#[command = 'i']
#[version >= 3]
#[filter_flag_gate = ADD_HEADERS]
struct InsertHeaderResponse {
    index: u32be,
    name: NulTerminatedString,
    value: NulTerminatedString,
}

The MTA modifies the message accordingly, then waits for another response from the milter.

ModifyHeaderResponse

The milter may send zero or more instances of ModifyHeaderResponse as a response to an EndOfBodyRequest if it wishes to change or delete headers in the e-mail message data.

#[command = 'm']
#[version >= 2]
#[filter_flag_gate = MODIFY_HEADERS]
struct ChangeHeaderResponse {
    index: u32be,
    name: NulTerminatedString,
    value: NulTerminatedString,
}

If the MTA finds a header at index index whose name case-insensitively matches name, that header's value is replaced with the contents of value. If value is empty (only consists of the NUL byte), the header is removed instead.

If the MTA does not find a header at index index whose name case-insensitively matches name, a new header is added with name and the contents of value. If value is empty, the header is skipped.

The MTA then waits for another response from the milter.

ChangeSenderResponse

The milter may send an instance of ChangeSenderResponse as a response to an EndOfBodyRequest if it wishes to change the sender of an e-mail message.

#[command = 'e']
#[version >= 6]
#[filter_flag_gate = CHANGE_SENDER]
struct ChangeSenderResponse {
    address: NulTerminatedString,
    optional {
        esmtp_options: NulTerminatedString,
    },
}

The MTA modifies the message accordingly, then waits for another response from the milter.

AddRecipientResponse

The milter may send zero or more instances of AddRecipientResponse as a response to an EndOfBodyRequest if it wishes to add additional e-mail addresses to the recipient list.

#[command = '+']
#[filter_flag_gate = ADD_RECIPIENTS]
struct AddRecipientResponse {
    address: NulTerminatedString,
}

The MTA modifies the message accordingly, then waits for another response from the milter.

AddRecipientArgsResponse

The milter may send zero or more instances of AddRecipientArgsResponse as a response to an EndOfBodyRequest if it wishes to add additional e-mail addresses with ESMTP arguments to the recipient list.

#[command = '2']
#[filter_flag_gate = ADD_RECIPIENTS_ARG]
struct AddRecipientArgsResponse {
    address: NulTerminatedString,
    optional {
        esmtp_options: NulTerminatedString,
    },
}

The MTA modifies the message accordingly, then waits for another response from the milter.

DeleteRecipientResponse

The milter may send zero or more instances of DeleteRecipientResponse as a response to an EndOfBodyRequest if it wishes to remove e-mail addresses from the recipient list.

#[command = '-']
#[filter_flag_gate = DELETE_RECIPIENTS]
struct DeleteRecipientResponse {
    address: NulTerminatedString,
}

The MTA modifies the message accordingly, then waits for another response from the milter.

ProgressResponse

The milter may send zero or more instances of ProgressResponse as a response to an EndOfBodyRequest if processing the message is taking a longer time and might risk running into a timeout configured on the MTA.

#[command = 'p']
struct ProgressResponse {
}

The MTA then waits for another response from the milter.

QuarantineResponse

The milter may send a QuarantineResponse as a response to an EndOfBodyRequest if it wishes the MTA to quarantine the message with the given text.

#[command = 'q']
#[version >= 3]
#[filter_flag_gate = QUARANTINE]
struct QuarantineResponse {
    quarantine_text: NulTerminatedString,
}

The MTA modifies its internal metadata about the message accordingly, then waits for another response from the milter.

ReplaceBodyResponse

The milter may send zero or more instances of ReplaceBodyResponse as a response to an EndOfBodyRequest if it wishes to change the e-mail body.

#[command = 'b']
#[filter_flag_gate = MODIFY_BODY]
struct ReplaceBodyResponse {
    body_chunk: NulTerminatedString,
}

The first instance of ReplaceBodyResponse replaces the message body with the contents of body_chunk; any subsequent ReplaceBodyResponse appends to it.

The MTA replaces the body accordingly, then waits for another response from the milter.

AcceptResponse

The milter may send an AcceptResponse as a response to most requests to signal to the MTA that it is no longer interested in the message and wishes for it to be delivered.

#[command = 'a']
struct AcceptResponse {
}

The MTA then stops passing further information about the message to the milter and, provided approval by other milters and internal decision processes is given, ultimately enqueues the message for delivery.

ContinueResponse

The milter may send a ContinueResponse as a response to most requests to signal to the MTA that it is still interested in the message and has not yet come across a reason to reject or discard it.

#[command = 'c']
struct ContinueResponse {
}

The MTA then continues processing the message.

A ContinueResponse should not be used as the final response to an EndOfBodyRequest, as the MTA cannot provide any further information about the message for the milter and it is time for the milter's final verdict.

SkipResponse

The milter may send a SkipResponse as a response to most requests to signal to the MTA that it is still interested in the message and has not yet come across a reason to reject or discard it; it is thus rather similar to ContinueResponse.

The actual semantics differ between Sendmail and Postfix:

  • With Sendmail, when replying with SkipResponse to a BodyRequest, skips all the remaining BodyRequests (as if the milter had responded to each of them with a ContinueResponse) and transmits an EndOfBodyRequest.

  • With Sendmail, when replying with SkipResponse to an earlier request (e.g. a ConnectRequest, a RcptToRequest, a HeaderRequest, etc.), Sendmail acts as if it were a ContinueResponse and "remembers" the SkipResponse for later: when it reaches the stage where the message body is to be transmitted to the milter, it only transmits the first chunk of the body in a BodyRequest, awaits the response (unless the NO_BODY_RESPONSE flag is set), then skips the rest of the body and sends an EndOfBodyRequest.

  • With Postfix, replying with SkipResponse to any request skips all immediately following requests of the same type (as if the milter had responded to each of them with a ContinueResponse. This is mainly interesting for requests that are issued multiple times per message, e.g. RcptToRequest, HeaderRequest and BodyRequest; it is equivalent to a ContinueResponse otherwise.

#[command = 's']
#[version >= 6]
#[protocol_flag_gate = KNOWS_SKIP]
struct SkipResponse {
}

The behavior between Sendmail and Postfix is consistent when responding with SkipResponse to a BodyRequest; it should probably be avoided in other contexts.

Just like ContinueResponse, it should not be used as the final response to an EndOfBodyRequest, as the MTA cannot provide any further information about the message for the milter and it is time for the milter's final verdict.

DiscardResponse

The milter may send an DiscardResponse as a response to most requests to signal to the MTA that it is no longer interested in the message and wishes for it to be discarded, i.e. deleted while pretending to the client that it will be delivered.

#[command = 'd']
struct DiscardResponse {
}

The MTA then stops passing further information about the message to the milter and ultimately discards the message.

RejectResponse

The milter may send a RejectResponse as a response to most requests to signal to the MTA that it is no longer interested in the message and wishes for it to be rejected, i.e. deleted while telling the client that it will not be delivered.

#[command = 'r']
struct RejectResponse {
}

The MTA then sends a rejection code (5xx) to the client, stops passing further information about the message to the milter and ultimately discards the message.

ReplyCodeResponse

The milter may send a ReplyCodeResponse as a response to most requests to signal to the MTA that it is no longer interested in the message and wishes for it to be rejected with a specific code, i.e. deleted while telling the client that it will not be delivered.

#[command = 'y']
struct ReplyCodeResponse {
    code: NulTerminatedString,
}

The MTA sends code as the response to the client; it must begin with a sequence of three digits 0-9 that represent the SMTP reply code and the code must be in the 400-499 (temporary failure) or 500-599 (permanent failure) ranges.

The MTA then stops passing further information about the message to the milter and ultimately discards the message.

ShutdownResponse

The milter may send a ShutdownResponse as a response to a ConnectRequest to signal to the MTA that it should immediately shut down the connection.

#[command = '4']
#[version >= 3]
struct ShutdownResponse {
}

The MTA then shuts down the connection. The client will probably try to deliver the message again later.

TempFailResponse

The milter may send a TempFailResponse as a response to most requests to signal to the MTA that it is no longer interested in the message and wishes for it to be rejected, i.e. deleted while telling the client that it will not be delivered.

#[command = 't']
struct TempFailResponse {
}

The MTA then sends a temporary rejection code (4xx) to the client, stops passing further information about the message to the milter and ultimately discards the message. The client will probably try to deliver this message again later.

ConnFailureResponse

The ConnFailureResponse is never actually transmitted from the milter to the MTA.

The command character f is reserved since version 6 for a potential future implementation.

Flags

The milter can tell the MTA:

  • which operations it might perform on an e-mail message (filter flags)
  • which events it is not interested in (protocol flags)

Since version 2, the values have been separated into filter_flags and protocol_flags in the NegotiationResponse, with values combined using bitwise OR.

The following filter_flags exist:

symbolic name value since version description
ADD_HEADERS 0x00000001 1 The milter might add (insert or append) headers to the message.
MODIFY_BODY 0x00000002 1 The milter might modify the body of the message.
ADD_RECIPIENTS 0x00000004 1 The milter might add recipients to the message.
DELETE_RECIPIENTS 0x00000008 1 The milter might remove recipients from the message.
MODIFY_HEADERS 0x00000010 2 The milter might change or delete the message's headers.
QUARANTINE 0x00000020 3 The milter might quarantine a message.
CHANGE_SENDER 0x00000040 6 The milter might change a message's sender.
ADD_RECIPIENTS_ARG 0x00000080 6 The milter might add recipients with ESMTP arguments to the message.
SET_MACROS 0x00000100 6 The milter might request that only a subset of macros is transmitted.

The following protocol_flags exist:

symbolic name value since version description
IGNORE_CONNECT 0x00000001 1 The MTA should not consult the milter when a new connection is opened.
IGNORE_HELO 0x00000002 1 The MTA should not consult the milter when a HELO command is received.
IGNORE_MAIL_FROM 0x00000004 1 The MTA should not consult the milter when a MAIL FROM command is received.
IGNORE_RCPT_TO 0x00000008 1 The MTA should not consult the milter when a RCPT TO command is received.
IGNORE_BODY 0x00000010 1 The MTA should not send the message body to the milter.
IGNORE_HEADERS 0x00000020 1 The MTA should not send the message headers to the milter.
IGNORE_END_HEADERS 0x00000040 2 The MTA should not send an end-of-headers request to the milter.
NO_HEADER_RESPONSE 0x00000080 3 The milter will not respond to HeaderRequests.
IGNORE_UNKNOWN 0x00000100 4.1 The MTA should not consult the milter when a non-standard command is received.
IGNORE_DATA 0x00000200 4.1 The MTA should not consult the milter when a DATA command is received.
KNOWS_SKIP 0x00000400 6 The MTA is telling the milter that it understands the SkipResponse.
RCPT_REJ 0x00000800 6 The MTA should also send rejected recipients to the milter.
NO_CONNECT_RESPONSE 0x00001000 6 The milter will not respond to ConnectRequests.
NO_HELO_RESPONSE 0x00002000 6 The milter will not respond to HeloRequests.
NO_MAIL_FROM_RESPONSE 0x00004000 6 The milter will not respond to MailFromRequests.
NO_RCPT_TO_RESPONSE 0x00008000 6 The milter will not respond to RcptToRequests.
NO_DATA_RESPONSE 0x00010000 6 The milter will not respond to DataRequests.
NO_UNKNOWN_RESPONSE 0x00020000 6 The milter will not respond to UnknownRequests.
NO_END_HEADERS_RESPONSE 0x00040000 6 The milter will not respond to EndOfHeadersRequests.
NO_BODY_RESPONSE 0x00080000 6 The milter will not respond to BodyRequests (only to the EndOfBodyRequest).
HEADER_LEADING_SPACE 0x00100000 6 The milter wants leading spaces before values left intact when reading headers and no additional leading spaces to be added when setting them.

If a NO_*_RESPONSE flag is set, right after the MTA sends the corresponding request to the milter, it pretends that the milter responded with a ContinueResponse and acts accordingly.

In version 1 of the protocol, these were supplied together in a value named flags:

symbolic name value description
ADD_HEADERS 0x00000001 The milter might add headers to the message.
MODIFY_BODY 0x00000002 The milter might modify the body of the message.
ADD_RECIPIENTS 0x00000004 The milter might add recipients to the message.
DELETE_RECIPIENTS 0x00000008 The milter might remove recipients from the message.
IGNORE_CONNECT 0x00000010 The MTA should not consult the milter when a new connection is opened.
IGNORE_HELO 0x00000020 The MTA should not consult the milter when a HELO command is received.
IGNORE_MAIL_FROM 0x00000040 The MTA should not consult the milter when a MAIL FROM command is received.
IGNORE_RCPT_TO 0x00000080 The MTA should not consult the milter when a RCPT TO command is received.
IGNORE_BODY 0x00000100 The MTA should not send the message body to the milter.
IGNORE_HEADERS 0x00000200 The MTA should not send the message headers to the milter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment