SMTPD-FILTERS(7)
π SMTPD-FILTERS(7) π
Bestie, welcome to SMTPD-FILTERS(7) from the Miscellaneous Information Manual β this is the page for smtpd-filters, aka the filtering API for the smtpd daemon. π«Ά
β¨ NAME β¨
smtpd-filters β the filtering API for the smtpd daemon. periodt. π
πΈ DESCRIPTION πΈ
Okay so the smtpd(8) daemon gives you a Simple Mail Transfer Protocol (SMTP) implementation, which means ordinary machines can become Mail eXchangers (MX). Iconic. But some features that MXes commonly use β like delivery reporting or spam filtering β are outside SMTP's scope and too complex to fit inside smtpd(8). π
Because an MX may need to provide those features, smtpd(8) provides an API to extend its behavior through smtpd-filters. slay. π
At runtime, smtpd(8) can report events to smtpd-filters, querying what it should answer to these events. This allows the decision logic to rely on third-party programs. no cap. β¨
π¦ DESIGN π¦
smtpd-filters are programs that run as unique standalone processes β they do not share smtpd(8) memory space. They are executed by smtpd(8) at startup and expected to run in an infinite loop, reading events and filtering requests from stdin(4), writing responses to stdout(4), and logging to stderr(4). They are not allowed to terminate. π
Because smtpd-filters are standalone programs that communicate with smtpd(8) through fd(4), they may run as different users than smtpd(8) and may be written in any language. smtpd-filters must not use blocking I/O β they must support answering asynchronously to smtpd(8). iykyk. π§
β¨ REPORT AND FILTER β¨
The API relies on two streams: report and filter. π
The report stream is a one-way stream that lets smtpd(8) inform smtpd-filters about events in real-time. Report events do not expect an answer from smtpd-filters β they're just meant to provide information. A filter should be able to replicate the smtpd(8) state for a session by gathering information coming from report events. No decision is ever taken by the report stream. facts. π
The filter stream is a two-way stream that lets smtpd(8) query smtpd-filters about what it should do with a session at a given phase. Filter requests do expect an answer from smtpd-filters; smtpd(8) will not let the session move forward until then. A decision must always be taken by the filter stream. periodt. π
It's sometimes possible to rely on filter requests to gather information, but because a response is expected by smtpd(8), this is more costly than using report events. The correct pattern for writing filters is: use report events to create a local state for a session, then use filter requests to take decisions based on that state. The only case when using filter requests instead of report events is correct is when a decision is required for the filter request and there is no need for more information than that of the event itself. that's the whole strategy, bestie. π«Ά
π PROTOCOL π
The protocol consists of human-readable lines exchanged between smtpd-filters and smtpd(8), through fd(4). π¦
The protocol begins with a handshake. First, smtpd(8) provides smtpd-filters with general configuration information in the form of key-value lines:
config|smtpd-version|7.9.0
config|protocol|0.7
config|smtp-session-timeout|300
config|subsystem|smtp-in
config|ready
Then, smtpd-filters register the stream, subsystem, and event they want to handle:
register|report|smtp-in|link-connect
register|ready
Finally, smtpd(8) emits report events and filter requests, expecting smtpd-filters to respond or not depending on the stream:
report|0.7|1576146008.006099|smtp-in|link-connect|7641df9771b4ed00|mail.openbsd.org|pass|199.185.178.25:33174|45.77.67.80:25
report|0.7|1576147242.200225|smtp-in|link-connect|7641dfb3798eb5bf|mail.openbsd.org|pass|199.185.178.25:31205|45.77.67.80:25
report|0.7|1576148447.982572|smtp-in|link-connect|7641dfc063102cbd|mail.openbsd.org|pass|199.185.178.25:24786|45.77.67.80:25
The character | may only appear in the last field of a payload β in which case it should be considered a regular character and not a separator. No other field may contain a |. don't mess with the separator rules, bestie. π
The list of subsystems and events, as well as the format of requests and responses, are documented in the sections below. π
π CONFIGURATION π
During the initial handshake, smtpd(8) emits a series of configuration keys and values. The list is meant to be ignored by smtpd-filters that do not require it and consumed gracefully by filters that do. respect the handshake. πΈ
There are currently three keys:
config|smtpd-version|7.9.0
config|protocol|0.7
config|smtp-session-timeout|300
config|subsystem|smtp-in
When smtpd(8) has sent all configuration keys, it emits the following line:
config|ready
β¨ REPORT EVENTS β¨
Okay bestie, so thereβs literally only one subsystem supported in the API right now: smtp-in. Thatβs it. One. Iconic minimalist vibes. π
Every report event gets generated by smtpd(8) as a single line β like this absolute unit right here:
report|0.7|1576146008.006099|smtp-in|link-connect|7641df9771b4ed00|mail.openbsd.org|pass|199.185.178.25:33174|45.77.67.80:25
So the format is giving protocol prefix realness. It contains the stream, the protocol version, the timestamp, the subsystem, the event, and the unique session identifier β all separated by |, no cap:
report|0.7|1576146008.006099|smtp-in|link-connect|7641df9771b4ed00
Then itβs followed by a suffix with all the event-specific parameters, also separated by |:
mail.openbsd.org|pass|199.185.178.25:33174|45.77.67.80:25
The list of events and their event-specific parameters for smtp-in is literally as follows β donβt skip, bestie, thereβs a lot but itβs all important:
link-connect:rdnsfcrdnssrcdest
This event is generated upon connection. β¨
rdns contains the reverse DNS hostname for the remote end, or an empty string if there isnβt one.
fcrdns contains the string pass or fail depending on if the remote end validates FCrDNS.
src contains either the IP address and port of the source address, in the format address:port, or the path to a UNIX socket in the format unix:/path.
dest holds either the IP address and port of the destination address, in the format address:port, or the path to a UNIX socket in the format unix:/path.
link-greeting:hostname
This event is generated upon display of the server banner.
hostname contains the hostname displayed in the banner.
link-identify:methodidentity
This event is generated upon HELO or EHLO command from the client.
method contains the string HELO or EHLO indicating the method used by the client.
identity contains the identity provided by the client.
link-tls:tls-string
This event is generated upon successful negotiation of TLS. π
tls-string contains a colon-separated list of TLS properties including the TLS version, the cipher suite used by the session and the cipher strength in bits.
link-disconnect
This event is generated upon disconnection of the client. Bye bestie! π
link-auth:resultusername
This event is generated upon an authentication attempt by the client.
result contains the string pass, fail or error depending on the result of the authentication attempt.
username contains the username used for the authentication attempt.
tx-reset:[message-id]
This event is generated when a transaction is reset.
If reset took place during a transaction, message-id contains the identifier of the transaction being reset.
tx-begin:message-id
This event is generated when a transaction is initiated.
message-id contains the identifier for the transaction.
tx-mail:message-idresultaddress
This event is generated when client emits MAIL FROM.
message-id contains the identifier for the transaction.
result contains ok if the sender was accepted, permfail if it was rejected or tempfail if it was rejected for a transient error.
address contains the e-mail address of the sender. The address is normalized and sanitized, the characters < and > are removed, along with any parameters to MAIL FROM.
tx-rcpt:message-idresultaddress
This event is generated when client emits RCPT TO.
message-id contains the identifier for the transaction.
result contains ok if the recipient was accepted, permfail if it was rejected or tempfail if it was rejected for a transient error.
address contains the e-mail address of the recipient. The address is normalized and sanitized, the characters < and > are removed, along with any parameters to RCPT TO.
tx-envelope:message-idenvelope-id
This event is generated when an envelope is accepted.
envelope-id contains the unique identifier for the envelope.
tx-data:message-idresult
This event is generated when client has emitted DATA.
message-id contains the unique identifier for the transaction.
result contains ok if server accepted the message for processing, permfail if it has not been accepted and tempfail if a transient error prevented message processing.
tx-commit:message-idmessage-size
This event is generated when a transaction has been accepted by the server.
message-id contains the unique identifier for the SMTP transaction.
message-size contains the size of the message submitted in the DATA phase of the SMTP transaction.
tx-rollback:message-id
This event is generated when a transaction has been rejected by the server. π
message-id contains the unique identifier for the SMTP transaction.
protocol-client:command
This event is generated for every command submitted by the client. It contains the raw command as received by the server.
command contains the command emitted by the client to the server.
protocol-server:response
This event is generated for every response emitted by the server. It contains the raw response as emitted by the server.
response contains the response emitted by the server to the client.
filter-report:filter-kindnamemessage
This event is generated when a filter emits a report.
filter-kind may be either builtin or proc depending on if the filter is an smtpd(8) builtin filter or a proc filter implementing the API.
name is the name of the filter that generated the report.
message is a filter-specific message.
filter-response:phaseresponse[param]
This event is generated when a filter responds to a filtering request.
phase contains the phase name for the request. The phases are documented in the next section.
response contains the response of the filter to the request, it is either one of proceed, report, reject, disconnect, junk or rewrite.
If specified, param is the parameter to the response.
timeout
This event is generated when a timeout happens for a session. β°
π FILTER REQUESTS π
Okay bestie, so rn there is only one subsystem supported in the API: smtp-in. That's the whole roster, no extras.
Filter requests are how smtpd(8) asks smtpd-filters what to do with a session at a particular phase. They also let smtpd-filters change the message content by adding, modifying, or suppressing input lines, kinda like what sed(1) or grep(1) would do. iconic behavior honestly π
Each filter request is generated by smtpd(8) as a single line similar to this one. Fields are separated by the | character:
filter|0.7|1576146008.006099|smtp-in|connect|7641df9771b4ed00|1ef1c203cc576e5d|mail.openbsd.org|199.185.178.25
The format is a protocol prefix containing the stream, the protocol version, the timestamp, the subsystem, the filtering phase, the unique session identifier, and an opaque token that the filter should provide in its response:
filter|0.7|1576146008.006099|smtp-in|connect|7641df9771b4ed00|1ef1c203cc576e5d
Then it's followed by a suffix with the phase-specific parameters of the filter request, also separated by |:
mail.openbsd.org|199.185.178.25
No cap: unlike with report events,smtpd(8)expects answers from filter requests and will not allow a session to move forward until the filter has instructedsmtpd(8)how to treat it. It is literally waiting on you π
For all phases except data-line, responses must follow the same construct: a message of type filter-result, followed by the unique session id, the opaque token, a decision, and optional decision-specific parameters:
filter-result|7641df9771b4ed00|1ef1c203cc576e5d|proceed
filter-result|7641df9771b4ed00|1ef1c203cc576e5d|reject|550 nope
The possible decisions for a filter-result message are documented below, bestie.
For the data-line phase, smtpd-filters are fed a stream of lines corresponding to the message to filter, terminated by a single dot:
filter|0.7|1576146008.006099|smtp-in|data-line|7641df9771b4ed00|1ef1c203cc576e5d|line 1
filter|0.7|1576146008.006103|smtp-in|data-line|7641df9771b4ed00|1ef1c203cc576e5d|line 2
filter|0.7|1576146008.006105|smtp-in|data-line|7641df9771b4ed00|1ef1c203cc576e5d|.
They are expected to return an output stream similarly terminated by a single dot. A filter may add to, suppress, modify, or echo back the lines it receives. Ultimately, smtpd(8) assumes that the message consists of the output from smtpd-filters.
Note that filters may be chained, and the lines that are input into a subsequent filter are the lines that are output from a previous filter. Chain them like bracelets, bestie π«Ά
The response to data-line requests use their own construct. A filter-dataline prefix, followed by the unique session identifier, the opaque token, and the output line as follows:
filter-dataline|7641df9771b4ed00|1ef1c203cc576e5d|line 1
filter-dataline|7641df9771b4ed00|1ef1c203cc576e5d|line 2
filter-dataline|7641df9771b4ed00|1ef1c203cc576e5d|.
The list of events and event-specific parameters for smtp-in are as follows:
connect:rdns srcβ this request is emitted after connection, before the banner is displayed.srccontains either the IP address of the source (a.b.c.dfor IPv4 or[x:x:x:x:x:x:x:x]IPv6) orlocal(for UNIX sockets).helo:identityβ this request is emitted after the client has emittedHELO.ehlo:identityβ this request is emitted after the client has emittedEHLO.starttls:tls-stringβ this request is emitted after the client has requestedSTARTTLS.auth:authβ this request is emitted after the client has requestedAUTH.mail-from:addressβ this request is emitted after the client has requestedMAIL FROM.rcpt-to:addressβ this request is emitted after the client has requestedRCPT TO.dataβ this request is emitted after the client has requestedDATA.data-line:lineβ this request is emitted for each line of input in theDATAphase. The lines are raw dot-escaped SMTP DATA input, terminated with a single dot.commitβ this request is emitted after the final single dot is received.
For every filtering phase, except data-line, the following decisions may be taken by a filter:
proceedβ no action is taken, session or transaction may be passed to the next filter.junkβ the session or transaction is marked as spam.smtpd(8)will prepend anX-Spamheader to the message.rejecterror β the command is rejected with the message error. The message must be a valid SMTP message including status code,5xxor4xx. Messages starting with a5xxstatus result in a permanent failure, those starting with a4xxstatus result in a temporary failure. Messages starting with a421status will result in a client disconnect. Dramatic but accurate πdisconnecterror β the client is disconnected with the message error. The message must be a valid SMTP message including status code,5xxor4xx. Messages starting with a5xxstatus result in a permanent failure, those starting with a4xxstatus result in a temporary failure.rewriteparameter β the command parameter is rewritten. This decision allows a filter to perform a rewrite of client-submitted commands before they are processed by the SMTP engine. parameter is expected to be a valid SMTP parameter for the command.reportparameter β generates a report with parameter for this filter.
π SEE ALSO π
π± HISTORY π±
smtpd-filters first appeared in OpenBSD 6.6. OG behavior, honestly β¨
okay bestie, that's the whole page β the Debian source/version situation is dated April 16, 2026, and you've been reading SMTPD-FILTERS(7) π
π¦ where this page comes from
- arch rolling: opensmtpd 7.9.0p0-1 Β· custom
- opensuse-leap 16.0: OpenSMTPD 7.9.0p0-bp160.1.1 Β· BSD-2-Clause AND BSD-3-Clause AND BSD-4-Clause AND ISC
- opensuse-leap 16.0: OpenSMTPD 7.9.0p0-bp160.1.1 Β· BSD-2-Clause AND BSD-3-Clause AND BSD-4-Clause AND ISC
- opensuse-leap 16.0: OpenSMTPD 7.9.0p0-bp160.1.1 Β· BSD-2-Clause AND BSD-3-Clause AND BSD-4-Clause AND ISC
- opensuse-leap 16.0: OpenSMTPD 7.9.0p0-bp160.1.1 Β· BSD-2-Clause AND BSD-3-Clause AND BSD-4-Clause AND ISC
- Copyright (c) 2008 Janne Johansson <jj@openbsd.org>
- Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net>
- Copyright (c) 2012 Gilles Chehade <gilles@poolp.org>
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms