SMTPD.CONF(5)

πŸ¦‹ SMTPD.CONF(5) πŸ¦‹

File Formats Manual, bestie. We're decoding the config for the mail daemon, okay? πŸ’Œ

πŸ’– NAME πŸ’–

smtpd.conf β€” SMTP daemon configuration file

✨ DESCRIPTION ✨

smtpd.conf is the configuration file for the mail daemon smtpd(8).

When mail arrives, each RCPT TO: command generates a mail envelope. If an envelope matches any of a pre-designated set of criteria (using the match directive), the message is accepted for delivery. A copy of the message, as well as its associated envelopes, is saved in the mail queue and later dispatched according to an associated set of actions (using the action directive). If an envelope does not match any options, it is rejected. The match rules are evaluated sequentially, with the first match winning. periodt.

The format of the configuration file is fairly flexible. The current line can be extended over multiple lines using a backslash (\). Comments can be put anywhere in the file using a hash mark (#) and extend to the end of the current line. Care should be taken when commenting out multi-line text: the comment is effective until the end of the entire block. Arguments not beginning with a letter, digit, or underscore, as well as reserved words (such as listen, match, and port), must be quoted. Arguments containing whitespace should be surrounded by double quotes ("). πŸ’…

A macro is defined with a command of the form name=value. The macro name can contain letters, digits, and underscores and cannot be a reserved word. Within unquoted arguments, the string $name is later expanded to value.

For example:

lan_addr = "192.168.0.1"
listen on $lan_addr
listen on $lan_addr tls auth

The syntax of smtpd.conf is described below. πŸ‘‡

🌸 action name method [options] 🌸

When the queue runner processes an envelope from the mail queue, it carries out the action name, selected by the match ... action directive when the message was received. The action directive provides configuration data for delivery attempts. Required lookups are performed at the time of each delivery attempt. Consequently, changing an action directive or the files it references and restarting the smtpd(8) daemon causes the changes to take effect for subsequent delivery attempts for the respective dispatcher name, even for messages that were already stuck in the queue prior to the configuration changes. No cap, the queue remembers. 😭

πŸ’Œ The delivery method parameter may be one of the following: πŸ’Œ

  • expand-only β€” Only accept the message if a delivery method was specified in an aliases or .forward file.
  • forward-only β€” Only accept the message if the recipient results in a remote address after the processing of aliases or forward file.
  • lmtp destination [rcpt-to] β€” Deliver the message to an LMTP server at destination. The location may be expressed as host:port or as a UNIX socket. Optionally, rcpt-to might be specified to use the recipient email address (after expansion) instead of the local user in the LMTP session as RCPT TO.
  • maildir [pathname] [junk] β€” Deliver the message to the maildir in pathname if specified, or by default to ~/Maildir. The pathname may contain format specifiers that are expanded before use (see FORMAT SPECIFIERS). If the junk argument is provided, the message will be moved to the Junk folder if it contains a positive X-Spam header. This folder will be created under pathname if it does not yet exist.
  • mbox β€” Deliver the message to the user's mbox with mail.local(8).
  • mda command β€” Delegate the delivery to a command that receives the message on its standard input (see MDA COMMANDS). The command may contain format specifiers that are expanded before use (see FORMAT SPECIFIERS).
  • relay β€” Relay the message to another SMTP server.

πŸŽ€ The local delivery methods support additional options: πŸŽ€

  • alias <table> β€” Use the mapping table for aliases(5) expansion.
  • ttl n{s|m|h|d} β€” Specify how long a message may remain in the queue.
  • user username β€” Specify the username for performing the delivery, to be looked up with getpwnam(3). This is used for virtual hosting where a single username is in charge of handling delivery for all virtual users. This option is not usable with the mbox delivery method. Only the delivery user's .forward file will be processed.
  • userbase <table> β€” Use the mapping table for user lookups instead of the getpwnam(3) function. The userbase does not apply to the user option.
  • virtual <table> β€” Use the mapping table for virtual expansion. The aliasing table format is described in table(5).
  • wrapper name β€” Use the wrapper specified in mda wrapper.

🌈 The relay delivery methods also support additional options: 🌈

  • backup β€” Operate as a backup mail exchanger delivering messages to any mail exchanger with higher priority.
  • backup mx name β€” Operate as a backup mail exchanger delivering messages to any mail exchanger with higher priority than mail exchanger identified as name.
  • ca caname β€” For secure connections, use the certificate authority associated with caname (declared in a ca directive) to validate the server's identity.
  • helo heloname β€” Advertise heloname as the hostname to other mail exchangers during the HELO phase.
  • helo-src <table> β€” Use the mapping table to look up a hostname matching the source address, to advertise during the HELO phase.
  • domain <domains> β€” Do not perform MX lookups but look up destination domain in domains and use matching relay url as relay host.
  • host relay-url β€” Do not perform MX lookups but relay messages to the relay host described by relay-url. The format for relay-url is [proto://[label@]]host[:port]. The following protocols are available:
    • smtp β€” Normal SMTP session with opportunistic STARTTLS (the default).
    • smtp+tls β€” Normal SMTP session with mandatory STARTTLS.
    • smtp+notls β€” Plain text SMTP session without TLS.
    • lmtp β€” LMTP session. port is required.
    • smtps β€” SMTP session with forced TLS on connection. The default port is 465. Unless noted, port defaults to 25. The label corresponds to an entry in a credentials table, as documented in table(5). It is used with the smtp+tls and smtps protocols for authentication. Server certificates for those protocols are verified by default.
  • pki pkiname β€” For secure connections, use the certificate associated with pkiname (declared in a pki directive) to prove the client's identity to the remote mail server.
  • srs β€” When relaying a mail resulting from a forward, use the Sender Rewriting Scheme to rewrite sender address.
  • tls [no-verify] β€” Require TLS to be used when relaying, using mandatory STARTTLS by default. When used with a smarthost, the protocol must not be smtp+notls://. If no-verify is specified, do not require a valid certificate.
  • protocols protostr β€” Define the protocol versions to be used for TLS sessions. Refer to the tls_config_parse_protocols(3) manpage for the format of protostr.
  • ciphers cipherstr β€” Define the list of ciphers that may be used for TLS sessions. Refer to the tls_config_set_ciphers(3) manpage for the format of cipherstr.
  • auth < table > β€” use the mapping table to connect to relay-url with credentials. This one only works if you also have the host option set, periodt. The credential table format? It's all in table(5), bestie. πŸ’…
    • mail-from mailaddr β€” sets mailaddr as the MAIL FROM address in the SMTP transaction. Like, that's the sender identity, no cap. πŸ“§
    • src sourceaddr | < sourceaddr > β€” use the string or list table sourceaddr for the source IP address. Super handy on machines with multiple interfaces. If the list has more than one address, all of them get used, and traffic is routed as efficiently as possible. Efficient queen. πŸ‘‘

  • admd authservid β€” sets the Administrative Management Domain this mail server belongs to. The authservid gets forwarded to filters, which use it to identify or mark authentication-results headers. If you omit it, it defaults to the server name. Slay. ✨
  • bounce warn-interval delay [, delay ...] β€” sends warning messages to the envelope sender when temporary delivery failures keep a message in the queue for longer than delay. Each delay is a positive decimal integer plus a unit: s, m, h, or d. You can specify at most four delay parameters. The default is "bounce warn-interval 4h", which sends a single warning after four hours. So like, don't ghost the sender. 😭
  • ca caname cert cafile β€” associates the Certificate Authority (CA) certificate file cafile with the ca entry caname. You can reference that ca entry in listen on and action ... relay rules. Secure vibes only. πŸ”
  • filter chain-name chain {filter-name [, ...]} β€” registers a chain of filters named chain-name, made up of the filters listed in filter-name. Filters in a chain run in the order they're declared, for each phase they're registered for. You can use a filter chain in place of a filter for any directive except filter chains themselves. So meta. πŸŒ€
  • filter filter-name phase phase-name match conditions decision β€” registers a filter called filter-name. A decision about what to do with the mail happens at phase phase-name when matching conditions. Phases, matching conditions, and decisions are all described in MAIL FILTERING, below. Go read that. πŸ‘€
  • filter filter-name proc proc-name β€” registers a "proc" filter named filter-name, backed by the proc-name process. Like, it's a whole process behind the scenes. πŸ’…
  • filter filter-name proc-exec command β€” registers and executes a "proc" filter named filter-name from command, conformant with the smtpd-filters(7) API. If command starts with a slash, it's executed with an absolute path; otherwise it runs from /usr/libexec/opensmtpd. Path logic, but make it make sense. πŸ“‚
  • include "pathname" β€” replaces this directive with the content of the additional configuration file at the absolute pathname. Like a config import, bestie. πŸ“₯
  • listen on interface [family] [options] β€” listens on the interface for incoming connections, using the same syntax as ifconfig(8). The interface parameter can also be an interface group, an IP address, or a domain name. You can optionally restrict listening to a specific address family, either inet4 or inet6. The options are as follows:
    • auth [< authtable >] β€” supports SMTPAUTH: clients can only start SMTP transactions after successful authentication. Users are authenticated against either their own normal login credentials or a credentials table authtable, whose format is described in table(5). Auth wall, but make it chic. πŸ”
    • auth-optional [< authtable >] β€” supports SMTPAUTH optionally: clients don't have to authenticate, but they can. This lets a listen on directive both accept incoming mail from untrusted senders and permit outgoing mail from authenticated users (using match auth). Use it when you can't listen on a separate port (usually the submission port, 587) for users to authenticate. Flexible queen. πŸ’β€β™€οΈ
    • ca caname β€” for secure connections, use the CA certificate associated with caname (declared in a ca directive) as the CA certificate when verifying client certificates. Trust issues, but the secure kind. πŸ”’
    • filter name β€” applies filter name on connections handled by this listener. Filter it, bestie. 🧚
    • hostname hostname β€” uses hostname in the greeting banner instead of the default server name. Customize that intro. ✨
    • hostnames < names > β€” overrides the server name for specific addresses. The names table maps IP addresses to hostnames. If the address where the connection arrives is in the mapping, the associated hostname is used. IP-to-name glow-up. 🌈
    • mask-src β€” omits the from part when prepending Received headers. Like, hide the source a little. πŸ™ˆ
    • no-dsn β€” disables the DSN (Delivery Status Notification) extension. No delivery receipts, periodt. 🚫
    • pki pkiname β€” for secure connections, use the certificate associated with pkiname (declared in a pki directive) to prove a mail server's identity. You can use this option multiple times to provide alternate certificates for SNI. Cert flex. πŸ’…
    • port [port] β€” listens on the given port instead of the default port 25. Port control, no cap. πŸ”Œ
    • proxy-v2 β€” supports the PROXYv2 protocol, appropriately rewriting the source address received from proxy. Proxy glow-up. πŸͺž
    • received-auth β€” in Received headers, reports whether the session was authenticated and by which local user. Receipts on the receipts. 🧾
    • senders < users >[masquerade] β€” looks up the authenticated user in the users mapping table to find the email addresses that user is allowed to submit mail as. Plus, if the masquerade option is provided, the From header is rewritten to match the sender provided in the SMTP session. Identity switch, but make it authorized. 🎭
    • smtps [verify] β€” supports SMTPS, by default on port 465. With the verify option, clients must also provide a valid certificate to establish an SMTP session. Secure socket, but make it verify. πŸ”
Mutually exclusive with tls β€” pick one, bestie. πŸ’…
  • tag tag β€” clients connecting to the listener are tagged with the given tag. Tagged, you're it. 🏷️
  • tls β€” supports STARTTLS, by default on port 25. TLS but make it start. ✨
Mutually exclusive with smtps β€” same energy. πŸ’…
  • tls-require [verify] β€” like tls, but forces clients to establish a secure connection before they're allowed to start an SMTP transaction. With the verify option, clients must also provide a valid certificate to establish an SMTP session. No secure connection, no entry. 🚫
  • protocols protostr β€” defines the protocol versions to be used for TLS sessions. Refer to the tls_config_parse_protocols(3) manpage for the format of protostr. Protocol picker, iykyk. πŸ“‘
  • ciphers cipherstr β€” defines the list of ciphers that may be used for TLS sessions. Refer to the tls_config_set_ciphers(3) manpage for the format of cipherstr. Cipher suite, but make it curated. πŸ’Ž

  • listen on socket [options] β€” listens for incoming SMTP connections on the Unix domain socket /run/smtpd.sock. This happens by default, even if the directive is absent. Socket slay. πŸ”Œ The options are as follows:
    • filter name β€” applies filter name on connections handled by this listener. Same filter energy. 🧚
    • mask-src β€” omits the from part when prepending Received headers. Hide the source. πŸ™ˆ
    • no-dsn β€” disables the DSN (Delivery Status Notification) extension. No delivery receipts. 🚫
    • tag tag β€” clients connecting to the listener are tagged with the given tag. Tagged. 🏷️
  • match options action name β€” bestie, this is the glow-up clause ✨ if even ONE mail envelope matches the options of a single match action directive, then boom: we accept the incoming message, drop a copy into every matching envelope, and atomically save those envelopes to the mail spool so the respective dispatcher name can deal with them later. And that dispatcher? It has to be declared beforehand with the action directive. No declaring it after, that's not the vibe. πŸ‘‘

The matching options below are all supported, and every single one of them can be negated with !:

  • [ !] for any β€” session may address literally any destination. Anything. Anywhere. Periodt. πŸ’…
  • [ !] for local β€” session may address any local domain. This is the default, so you can just... omit it. It's the chill one. 🫢
  • [ !] for domain [regex] / domain | <domain> β€” session may address the string or list table domain. The regex keyword is required if domain is a regular expression or a table of regular expressions. Don't forget it, she will not work otherwise. πŸ’€
  • [ !] for rcpt-to [regex] / recipient | <recipient> β€” session may address the string or list table recipient. The regex keyword is required if recipient is a regular expression or a table of regular expressions.
  • [ !] from any β€” session may originate from any source. Feral. Unhinged. We love that for her. 🌈
  • [ !] from auth β€” session may originate from any authenticated user, no matter the source IP address. IP who? Never heard of her. πŸ¦‹
  • [ !] from auth [regex] / user | <user> β€” session may originate from authenticated user or user list user, no matter the source IP address. The regex keyword is required if user is a regular expression or a table of regular expressions.
  • [ !] from local β€” session may only originate from a local IP address, or from the local enqueuer. This is the default, and may be omitted. Homebody energy, honestly. πŸŽ€
  • [ !] from mail-from [regex] / sender | <sender> β€” session may originate from sender or sender list sender, no matter the source IP address. The regex keyword is required if sender is a regular expression or a table of regular expressions.
  • [ !] from rdns β€” session may only originate from an IP address that resolves to a reverse DNS. If she can't reverse-lookup, she can't come in. Bye. πŸ’…
  • [ !] from rdns [regex] / hostname | <hostname> β€” session may only originate from an IP address that resolves to a reverse DNS matching string or list string hostname. The regex keyword is required if hostname is a regular expression or a table of regular expressions.
  • [ !] from socket β€” session may only originate from the local enqueuer. Local only, no long-distance relationships. πŸ“΅
  • [ !] from src [regex] / address | <address> β€” session may only originate from string or list table address, which can be a specific address or a subnet expressed in CIDR-notation. The regex keyword is required if address is a regular expression or a table of regular expressions.

Plus, bestie, the following transaction options can also be matched:

  • [ !] auth β€” matches transactions which have been authenticated. Verified and valid, she ate. βœ…
  • [ !] auth [regex] / username | <username> β€” matches transactions which have been authenticated for user or user list username. The regex keyword is required if username is a regular expression or a table of regular expressions.
  • [ !] helo [regex] / helo-name | <helo-name> β€” session's HELO / EHLO should match the string or list table helo-name. The regex keyword is required if helo-name is a regular expression or a table of regular expressions.
  • [ !] mail-from [regex] / sender | <sender> β€” transaction's MAIL FROM should match the string or list table sender. The regex keyword is required if sender is a regular expression or a table of regular expressions.
  • [ !] rcpt-to [regex] / recipient | <recipient> β€” transaction's RCPT TO should match the string or list table recipient. The regex keyword is required if recipient is a regular expression or a table of regular expressions.
  • [ !] tag [regex] tag β€” matches transactions tagged with the given tag. The regex keyword is required if tag is a regular expression.
  • [ !] tls β€” transaction should take place in a TLS channel. Secure the bag, fr fr. πŸ”’βœ¨

  • match options reject β€” reject the incoming message during the SMTP dialogue. Rude but efficient. The same options are supported as for the match action directive β€” same menu, different ending. 🚫
  • mda wrapper name command β€” associate command with the mail delivery agent wrapper named name. When a local delivery specifies a wrapper, the command associated with that wrapper gets executed instead. The command may contain format specifiers (go peek at FORMAT SPECIFIERS). πŸ‘€
  • mta max-deferred number β€” when delivery to a given host is suspended because of temporary failures, cache at most number envelopes for that host, so they can be delivered as soon as another delivery succeeds to that host. The default is 100. Hoarding, but make it strategic. πŸ—‚οΈ
  • pki pkiname cert certfile β€” associate certificate file certfile with pki entry pkiname. The pki entry defines a keypair configuration that can be referenced in listener rules and relay actions.

A certificate chain may be created by appending one or many certificates β€” including a Certificate Authority certificate β€” to certfile. The creation of certificates is documented in starttls(8). πŸ“œ

  • pki pkiname key keyfile β€” associate the key located in keyfile with pki entry pkiname. The other half of the power couple. πŸ”‘πŸ’ž
  • pki pkiname dhe params β€” specify the DHE parameters to use for DHE cipher suites with pki entry pkiname. Valid parameter values are none, legacy, and auto. For legacy, a fixed key length of 1024 bits is used, whereas for auto, the key length is determined automatically. The default is none, which disables DHE cipher suites. So like, if you want them, you have to actually say so. πŸ™ˆ
  • proc proc-name command β€” register an external process named proc-name from command, conformant with the smtpd-filters(7) API. Such processes may be used to share the same instance between multiple filters. If command starts with a slash it is executed with an absolute path, otherwise it will be run from /usr/libexec/opensmtpd. Slash or it's a house guest. 🏠
  • queue compression β€” store queue files in a compressed format. This may be useful to save disk space. Tiny files, big slay. πŸ—œοΈ
  • queue encryption [key] β€” encrypt queue files with EVP_aes_256_gcm(3). If no key is specified, it is read with getpass(3). If the string stdin or a single dash (-) is given instead of a key, the key is read from the standard input. Secrets stay secret, iykyk. 🀫
  • queue ttl delay β€” set the default expiration time for temporarily undeliverable messages, given as a positive decimal integer followed by a unit: s, m, h, or d. The default is four days (4d). That's the grace period, bestie, use it wisely. ⏳
  • smtp ciphers control β€” set the control string for SSL_CTX_set_cipher_list(3). The default is HIGH:!aNULL:!MD5. πŸ’
  • smtp limit max-mails count β€” limit the number of messages to count for each session. The default is 100.
  • smtp limit max-rcpt count β€” limit the number of recipients to count for each transaction. The default is 1000.
  • smtp max-message-size size β€” bestie if a message is bigger than size, it gets rejected, no debate. Give size as a positive number of bytes, or as a string that gets parsed with scan_scaled(3). The default is 35M. πŸ’…
  • smtp sub-addr-delim character β€” when we're resolving the local part of a local email address, just ignore this ASCII character and literally everything after it. The default is +.
  • srs key [backup] secret β€” sets the secret key to use for SRS, aka the Sender Rewriting Scheme. You can also pass the backup option to set a secret key used as a fallback for SRS β€” which is how you do SRS key rotation, slay.
  • srs ttl delay β€” sets the time-to-live delay for SRS envelopes. Once that delay passes, a bounce reply to the SRS address gets discarded, which limits the risk of forged addresses. Default is four days (4d).
  • table name [type:]pathname β€” tables are how you hand smtpd(8) extra config info, as lists or key-value mappings. What the entries actually look like depends on what the table is for β€” go read table(5) for the full exhaustive documentation, fr fr.

Every single table gets identified by an arbitrary, unique name.

If the type is db, the info lives in a file created with makemap(8). If it's file or you just omit it entirely, the info lives in a plain text file using the format described in table(5). And heads up: the pathname to the file must be absolute. πŸ”₯

  • table name {value [, ...]} β€” instead of pointing at a separate file, just declare a list table right here with the given static *value*s. It has to contain at least one value, and you can declare multiple values as a comma-separated list (whitespace optional, do whatever).
  • table name {key=value [, ...]} β€” same energy, but now it's a mapping table declared inline with static key-value pairs. At least one pair required, and again you can declare multiple pairs as a comma-separated list (whitespace optional).

✨ MAIL FILTERING ✨

okay so in a regular workflow, smtpd(8) can accept or reject a message based only on what's in the envelopes. Its decisions are about handling the message, not about handling an active session. πŸ¦‹

Filtering levels that whole thing up β€” it lets smtpd(8) stop at each phase of an SMTP session, check that conditions are met, and then decide whether a session gets to move forward. πŸ‘‘

With filtering, a session can be interrupted at any phase before an envelope is complete. And a message can also be rejected after being submitted, regardless of whether the envelope was accepted or not. no cap.

The following phases are currently supported:

  • connect β€” upon connection, before a banner is displayed
  • helo β€” after the HELO command is submitted
  • ehlo β€” after the EHLO command is submitted
  • mail-from β€” after the MAIL FROM command is submitted
  • rcpt-to β€” after the RCPT TO command is submitted
  • data β€” after the DATA command is submitted
  • commit β€” after the message is fully submitted

At each phase, various conditions may be matched. The fcrdns, rdns, and src data are available in all phases, but the other data has to have already been submitted before it's available. πŸ™ˆ

  • fcrdns β€” forward-confirmed reverse DNS is valid
  • rdns β€” session has a reverse DNS
  • rdns <table> β€” session has a reverse DNS in table
  • src <table> β€” source address is in table
  • helo <table> β€” helo name is in table
  • auth β€” session is authenticated
  • auth <table> β€” session username is in table
  • mail-from <table> β€” sender address is in table
  • rcpt-to <table> β€” recipient address is in table

All of these conditions can be negated by sticking an exclamation mark in front of them:

  • !fcrdns β€” forward-confirmed reverse DNS is invalid

And any condition that uses a table can say "hey, this table is full of regular expressions" by prefixing the table name with the keyword regex. πŸ’–

  • helo regex <table> β€” helo name matches a regex in table

Finally, there's a whole menu of decisions you can take:

  • bypass β€” the session or transaction bypasses filters
  • disconnect message β€” the session is disconnected with message
  • junk β€” the session or transaction is junked, i.e., an X-Spam: yes header is added to any messages
  • reject message β€” the command is rejected with message
  • rewrite value β€” the command parameter is rewritten with value

Decisions that involve a message require that the message be RFC valid, meaning they should either start with a 4xx or 5xx status code. Decisions can be taken at any phase, though junking can only happen before a message is committed. ⚠️


🌈 FORMAT SPECIFIERS 🌈

Some configuration directives support expansion of their parameters at runtime. Those directives (for example action maildir, action mda) may use format specifiers, which get expanded before delivery or relaying. Here's everything currently supported:

  • %{sender} β€” sender email address, may be empty string
  • %{sender.user} β€” user part of the sender email address, may be empty
  • %{sender.domain} β€” domain part of the sender email address, may be empty
  • %{rcpt} β€” recipient email address
  • %{rcpt.user} β€” user part of the recipient email address
  • %{rcpt.domain} β€” domain part of the recipient email address
  • %{dest} β€” recipient email address after expansion
  • %{dest.user} β€” user part after expansion
  • %{dest.domain} β€” domain part after expansion
  • %{user.username} β€” local user
  • %{user.directory} β€” home directory of the local user
  • %{mbox.from} β€” name used in mbox From separator lines
  • %{mda} β€” mda command, only available for mda wrappers

Expansion formats also support partial expansion using the optional bracket notations with substring offset. For example, with recipient domain example.org:

  • %{rcpt.domain[0]} β€” expands to e
  • %{rcpt.domain[1]} β€” expands to x
  • %{rcpt.domain[8:]} β€” expands to org
  • %{rcpt.domain[-3:]} β€” expands to org
  • %{rcpt.domain[0:6]} β€” expands to example
  • %{rcpt.domain[0:-4]} β€” expands to example

On top of that, modifiers may be applied to the token. For example, with recipient User+Tag@Example.org:

  • %{rcpt:lowercase} β€” expands to user+tag@example.org
  • %{rcpt:uppercase} β€” expands to USER+TAG@EXAMPLE.ORG
  • %{rcpt:strip} β€” expands to User@Example.org
  • %{rcpt:lowercase|strip} β€” expands to user@example.org

Modifiers and partial expansion can be used together. In that case, the modifiers get applied before the partial expansion. For example, with sender owner-tech+M123456@openbsd.org:

  • %{sender.user[6:]:strip} β€” expands to tech

For security concerns, expanded values are sanitized and potentially dangerous characters get replaced with :. In situations where you genuinely want them, the raw modifier may be applied. For example, with recipient user+t?g@example.org:

  • %{rcpt} β€” expands to user+t:g@example.org
  • %{rcpt:raw} β€” expands to user+t?g@example.org

πŸŽ€ MDA COMMANDS πŸŽ€

When an action delivery method is mda, smtpd(8) runs the associated command for the delivery, with the mail content provided via standard input. The command is expected to read all the mail content, bestie, don't leave crumbs. πŸ“

The exit code of the command reports the outcome of the delivery: status 0 (EX_OK) is a successful delivery; status 71 (EX_OSERR) and 75 (EX_TEMPFAIL) are temporary failures; and every other exit status is considered a permanent failure. πŸ’€

The following environment variables are set:

  • DOMAIN β€” the recipient domain.
  • EXTENSION β€” the sub address of the recipient (may be unset).
  • HOME β€” the delivery user's login directory.
  • LOCAL β€” the local part of the recipient user address.
  • LOGNAME β€” the login name of the user.
  • ORIGINAL_RECIPIENT β€” the address of the original recipient.
  • PATH β€” set to _PATH_DEFPATH. Traditionally /usr/bin:/bin, but expanded to include /usr/sbin, /sbin, /usr/X11R6/bin, /usr/local/bin, and /usr/local/sbin in OpenBSD.
  • RECIPIENT β€” the address of the final recipient.
  • SENDER β€” the address of the sender (might be empty).
  • SHELL β€” set to /bin/sh.
  • USER β€” synonym of LOGNAME for backwards compatibility.

🌸 FILES 🌸

  • /etc/opensmtpd/smtpd.conf β€” the default smtpd(8) configuration file.
  • /etc/opensmtpd/mailname β€” if this file exists, the first line is used as the server name. Otherwise, the server name is derived from the local hostname returned by gethostname(3) β€” either directly if it's a fully qualified domain name, or by retrieving the associated canonical name through getaddrinfo(3).
  • /run/smtpd.sock β€” Unix domain socket for incoming SMTP connections.
  • /var/spool/smtpd/ β€” spool directories for mail during processing.

✨ EXAMPLES ✨

Okay bestie, real talk β€” this is where it gets fun. The default smtpd.conf that ships with OpenBSD listens on the loopback network interface (lo0) and allows mail from users and daemons on the local machine, plus it lets email go out to remote servers. But like... that's just the starter pack. Let's look at some glow-ups. πŸ’…


πŸŽ€ Example 1: everything outbound goes through a remote relay

This first one is basically the default config but with a twist β€” all outgoing mail gets forwarded to a remote SMTP server. To do that you need a secrets file holding a username and password:

# touch /etc/opensmtpd/secrets
# chmod 640 /etc/opensmtpd/secrets
# chown root:smtpd /etc/opensmtpd/secrets
# echo "bob username:password" > /etc/opensmtpd/secrets

Then smtpd.conf would look like this:

table aliases file:/etc/opensmtpd/aliases
table secrets file:/etc/opensmtpd/secrets
listen on lo0
action "local_mail" mbox alias <aliases>
action "outbound" relay host smtp+tls://bob@smtp.example.com \
     auth <secrets>
match from local for local action "local_mail"
match from local for any action "outbound"

πŸ” Example 2: auth-only, with a real certificate

In this second example, the goal is to permit mail delivery and relaying only for users that can authenticate (using their normal login credentials). You need an RSA certificate to prove the server's identity. The mail server listens on all interfaces the default routes point to. Mail with a local destination gets sent to an external MDA.

First, the RSA certificate gets created:

# openssl genrsa -out /etc/ssl/private/mail.example.com.key 4096
# openssl req -new -x509 -key /etc/ssl/private/mail.example.com.key \
     -out /etc/ssl/mail.example.com.crt -days 365
# chmod 600 /etc/ssl/mail.example.com.crt
# chmod 600 /etc/ssl/private/mail.example.com.key

In the example above, a certificate valid for one year was created. The configuration file would look like this:

pki mail.example.com cert "/etc/ssl/mail.example.com.crt"
pki mail.example.com key "/etc/ssl/private/mail.example.com.key"
table aliases file:/etc/opensmtpd/aliases
listen on lo0
listen on egress tls pki mail.example.com auth
action mda_with_aliases mda "/path/to/mda -f -" alias <aliases>
action mda_without_aliases mda "/path/to/mda -f -"
action "outbound" relay
match for local action mda_with_aliases
match from any for domain example.com action mda_without_aliases
match for any action "outbound"
match auth from any for any action "outbound"

πŸ¦‹ Example 3: DKIM signing with opensmtpd-filter-dkimsign

For sites that wish to sign messages using DKIM, the following example uses opensmtpd-filter-dkimsign for DKIM signing:

table aliases file:/etc/opensmtpd/aliases
filter "dkimsign" proc-exec "filter-dkimsign -d <domain> -s <selector> \
     -k /etc/opensmtpd/dkim/private.key" user _dkimsign group _dkimsign
listen on socket filter "dkimsign"
listen on lo0 filter "dkimsign"
action "local_mail" mbox alias <aliases>
action "outbound" relay
match for local action "local_mail"
match for any action "outbound"

🌈 Example 4: rspamd integration

Alternatively, the opensmtpd-filter-rspamd package may be used to provide integration with rspamd, a third-party daemon which provides multiple antispam features as well as DKIM signing. As well as configuring rspamd itself, it requires use of the proc-exec keyword:

filter "rspamd" proc-exec "filter-rspamd"

πŸ‘‘ Example 5: blocking forged local-domain mail

Sites that accept non-local messages may be able to cut down on the volume of spam received by rejecting forged messages that claim to be from the local domain. The following example uses a list table other-relays to specify the IP addresses of relays that may legitimately originate mail with the owner's domain as the sender.

table aliases file:/etc/opensmtpd/aliases
table other-relays file:/etc/opensmtpd/other-relays
listen on lo0
listen on egress
action "local_mail" mbox alias <aliases>
action "outbound" relay
match for local action "local_mail"
match for any action "outbound"
match !from src <other-relays> mail-from "@example.com" for any \
      reject
match from any for domain example.com action "local_mail"

πŸ‘€ SEE ALSO πŸ‘€


🌸 HISTORY 🌸

smtpd(8) first appeared in OpenBSD 4.6.


And that's the whole page, bestie β€” we ate and left absolutely zero crumbs. πŸ’– This one's from the OpenBSD crew, dated April 3 2026, and it's the SMTPD.CONF(5) page. Go configure something iconic. πŸŽ€βœ¨πŸ¦‹

πŸ“¦ where this page comes from