maddy Documentation

️ Composable all-in-one mail server.

6,111 stars Go Go 72 files · ~49,185 tokens #dkim#dmarc#email#imap#mail#smtp#spf
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy maddy on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name maddy -p 25:25 --restart always foxcpp/maddy:latest

Technical Documentation: foxcpp/maddy

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

Maddy Mail Server

Composable all-in-one mail server.

Maddy Mail Server implements all functionality required to run a e-mail
server. It can send messages via SMTP (works as MTA), accept messages via SMTP
(works as MX) and store messages while providing access to them via IMAP.
In addition to that it implements auxiliary protocols that are mandatory
to keep email reasonably secure (DKIM, SPF, DMARC, DANE, MTA-STS).

It replaces Postfix, Dovecot, OpenDKIM, OpenSPF, OpenDMARC and more with one
daemon with uniform configuration and minimal maintenance cost.

Note: IMAP storage is "beta". If you are looking for stable and
feature-packed implementation you may want to use Dovecot instead. maddy still
can handle message delivery business.



Overview (`cmd/README.md`)

maddy executables

maddy

Main server executable.

maddy-pam-helper, maddy-shadow-helper

Utilities compatible with the auth.external module that call libpam or read
/etc/shadow on Unix systems.


Overview (`contrib/README.md`)

Community contributed resources

Disclaimer: Nothing inside subdirectories here is directly supported by Maddy
Mail Server maintainers. Some community members may be able to help you or not.

  • Kubernetes helm chart is maintained by @acim.

Overview (`dist/README.md`)

Distribution files for maddy

Disclaimer: Most of the files here are maintained in a "best-effort" way.
That is, they may break or become outdated from time to time. Caveat emptor.

integration + scripts

These directories provide pre-made configuration snippets suitable for
easy integration with external software.

Usually, this is what you use when you put import integration/something in
your config.

systemd unit

maddy.service launches using default config path (/etc/maddy/maddy.conf).
[email protected] launches maddy using custom config path. E.g.
[email protected] will use /etc/maddy/foo.conf.

Additionally, unit files apply strict sandboxing, limiting maddy permissions on
the system to a bare minimum. Subset of these options makes it impossible for
privileged authentication helper binaries to gain required permissions, so you
may have to disable it when using system account-based authentication with
maddy running as a unprivileged user.

fail2ban configuration

Configuration files for use with fail2ban. Assume either backend = systemd specified
in system-wide configuration or log file written to /var/log/maddy/maddy.log.

See https://github.com/foxcpp/maddy/wiki/fail2ban-configuration for details.

logrotate configuration

Meant for logs rotation when logging to file is used.

vim ftdetect/ftplugin/syntax files

Minimal supplement to make configuration files more readable and help you see
typos in directive names.


Overview (`internal/README.md`)

maddy source tree

Main maddy code base lives here. No packages are intended to be used in
third-party software hence API is not stable.

Subdirectories are organized as follows:

text
/
  auxiliary libraries
endpoint/
  modules - protocol listeners (e.g. SMTP server, etc)
target/
  modules - final delivery targets (including outbound delivery, such as
  target.smtp, remote)
auth/
  modules - authentication providers
check/
  modules - message checkers (module.Check)
modify/
  modules - message modifiers (module.Modifier)
storage/
  modules - local messages storage implementations (module.Storage)

Overview (`tests/README.md`)

maddy integration testing

Tests structure

The test library creates a temporary state and runtime directory, starts the
server with the specified configuration file and lets you interact with it
using a couple of convenient wrappers.

Running

To run tests, use go test -tags integration in this directory. Make sure to
have a maddy executable in the current working directory.
Use -integration.executable if the executable is named different or is placed
somewhere else.
Use -integration.coverprofile to pass -test.coverprofile your_value.RANDOM to test executable. See ./build_cover.sh to build a
server executable instrumented with coverage counters.


Overview (`cmd/maddy-pam-helper/README.md`)

maddy-pam-helper

External setuid binary for interaction with shadow passwords database or other
privileged objects necessary to run PAM authentication.

Building

It is really easy to build it using any GCC:

text
gcc pam.c main.c -lpam -o maddy-pam-helper

Yes, it is not a Go binary.

Installation

maddy-pam-helper is kinda dangerous binary and should not be allowed to be
executed by everybody but maddy's user. At the same moment it needs to have
access to read-protected files. For this reason installation should be done
very carefully to make sure to not introduce any security "holes".

First method

shell
chown maddy: /usr/bin/maddy-pam-helper
chmod u+x,g-x,o-x /usr/bin/maddy-pam-helper

Also maddy-pam-helper needs access to /etc/shadow, one of the ways to provide
it is to set file capability CAP_DAC_READ_SEARCH:

text
setcap cap_dac_read_search+ep /usr/bin/maddy-pam-helper

Second method

Another, less restrictive is to make it setuid-root (assuming you have both maddy user and group):

text
chown root:maddy /usr/bin/maddy-pam-helper
chmod u+xs,g+x,o-x /usr/bin/maddy-pam-helper

Third method

The best way actually is to create shadow group and grant access to
/etc/shadow to it and then make maddy-pam-helper setgid-shadow:

text
groupadd shadow
chown :shadow /etc/shadow
chmod g+r /etc/shadow
chown maddy:shadow /usr/bin/maddy-pam-helper
chmod u+x,g+xs /usr/bin/maddy-pam-helper

Pick what works best for you.

PAM service

maddy-pam-helper uses custom service instead of pretending to be su or sudo.
Because of this you should configure PAM to accept it.

Minimal example using local passwd/shadow database for authentication can be
found in [maddy.conf][maddy.conf] file.
It should be put into /etc/pam.d/maddy.


Overview (`cmd/maddy-shadow-helper/README.md`)

maddy-shadow-helper

External helper binary for interaction with shadow passwords database.
Unlike maddy-pam-helper it supports only local shadow database but it does
not have any C dependencies.

Installation

maddy-shadow-helper is kinda dangerous binary and should not be allowed to be
executed by everybody but maddy's user. At the same moment it needs to have
access to read-protected files. For this reason installation should be done
very carefully to make sure to not introduce any security "holes".

First method

shell
chown maddy: /usr/bin/maddy-shadow-helper
chmod u+x,g-x,o-x /usr/bin/maddy-shadow-helper

Also maddy-shadow-helper needs access to /etc/shadow, one of the ways to provide
it is to set file capability CAP_DAC_READ_SEARCH:

text
setcap cap_dac_read_search+ep /usr/bin/maddy-shadow-helper

Second method

Another, less restrictive is to make it setuid-root (assuming you have both maddy user and group):

text
chown root:maddy /usr/bin/maddy-shadow-helper
chmod u+xs,g+x,o-x /usr/bin/maddy-shadow-helper

Third method

The best way actually is to create shadow group and grant access to
/etc/shadow to it and then make maddy-shadow-helper setgid-shadow:

text
groupadd shadow
chown :shadow /etc/shadow
chmod g+r /etc/shadow
chown maddy:shadow /usr/bin/maddy-shadow-helper
chmod u+x,g+xs /usr/bin/maddy-shadow-helper

Pick what works best for you.


Overview (`docs/man/README.md`)

maddy manual pages

The reference documentation is maintained in the scdoc format and is compiled
into a set of Unix man pages viewable using the standard man utility.

See https://git.sr.ht/~sircmpwn/scdoc for information about the tool used to
build pages.
It can be used as follows:

text
scdoc < maddy-filters.5.scd > maddy-filters.5
man ./maddy-filters.5

build.sh script in the repo root compiles and installs man pages if the scdoc
utility is installed in the system.


Overview (`contrib/kubernetes/chart/README.md`)

maddy Helm chart for Kubernetes

This is just initial effort to run maddy within Kubernetes cluster. We have used Deployment resource which has some downsides
but at least this chart will allow you to install maddy relatively easily on your Kubernetes cluster. We have considered
StatefulSet and DaemonSet but such solutions would require much more configuration and in casae of DaemonSet also a TCP
load balancer in front of the nodes.

Requirement

In order to run maddy properly, you need to have TLS secret under name maddy present in the cluster. If you have commercial
certificate, you can create it by the following command:

sh
kubectl create secret tls maddy --cert=fullchain.pem --key=privkey.pem

If you use cert-manager, just create the secret under name maddy.

Replication

Default for this chart is 1 replica of maddy. If you try to increase this, you will probably get an error because of
the busy ports 25, 143, 587, etc. We do not support this feature at the moment, so please use just 1 replica. Like said
at the beginning of this document, multiple replicas would probably require to switch do DaemonSet which would further require
to have TCP load balancer and shared storage between all replicas. This is not supported by this chart, sorry.
This chart is used on one node cluster and then installation is straight forward, like described bellow, but if you have
multiple node cluster, please use taints and tolerations to select the desired node. This chart supports tolerations to
be set.

Configuration

Key Type Default Description
affinity object {}
fullnameOverride string ""
image.pullPolicy string "IfNotPresent"
image.repository string "foxcpp/maddy"
image.tag string ""
imagePullSecrets list []
nameOverride string ""
nodeSelector object {}
persistence.accessMode string "ReadWriteOnce"
persistence.annotations object {}
persistence.enabled bool false
persistence.path string "/data"
persistence.size string "128Mi"
podAnnotations object {}
podSecurityContext object {}
replicaCount int 1
resources object {}
securityContext object {}
service.type string "NodePort"
serviceAccount.annotations object {}
serviceAccount.create bool true
serviceAccount.name string ""
tolerations list []

Installing the chart

sh
helm upgrade --install maddy ./chart --set service.externapIPs[0]=1.2.3.4

1.2.3.4 is your public IP of the node.

maddy configuration

Feel free to tweak files/maddy.conf and files/aliases according to your needs.


Overview (`framework/config/lexer/README.md`)

caddyfile lexer copied from caddy project.

Taken from the following commit:

text
commit ed4c2775e46b924d4851e04cc281633b1b2c15af
Author: Alexander Danilov <[email protected]>
Date:   Wed Aug 21 20:13:34 2019 +0300

    main: log caddy version on start (#2717)

License of the original code is included in LICENSE.APACHE file in this
directory.

No signficant changes was made to the code (e.g. it is safe to update it from
caddy repo).

The code is copied because caddy brings quite a lot of dependencies we don't
use and this slows down many tools.


Index (`docs/index.md`)

Composable all-in-one mail server.

Maddy Mail Server implements all functionality required to run a e-mail
server. It can send messages via SMTP (works as MTA), accept messages via SMTP
(works as MX) and store messages while providing access to them via IMAP.
In addition to that it implements auxiliary protocols that are mandatory
to keep email reasonably secure (DKIM, SPF, DMARC, DANE, MTA-STS).

It replaces Postfix, Dovecot, OpenDKIM, OpenSPF, OpenDMARC and more with one
daemon with uniform configuration and minimal maintenance cost.

Note: IMAP storage is "beta". If you are looking for stable and
feature-packed implementation you may want to use Dovecot instead. maddy still
can handle message delivery business.



Docker (`docs/docker.md`)

Docker

Official Docker image is available from Docker Hub.

It expects configuration file to be available at /data/maddy.conf.

If /data is a Docker volume, then default configuration will be placed there
automatically. If it is used, then MADDY_HOSTNAME, MADDY_DOMAIN environment
variables control the host name and primary domain for the server. TLS
certificate should be placed in /data/tls/fullchain.pem, private key in
/data/tls/privkey.pem

DKIM keys are generated in /data/dkim_keys directory.

Image tags

  • latest - A latest stable release. May contain breaking changes.
  • X.Y - A specific feature branch, it is recommended to use these tags to
    receive bugfixes without the risk of feature-related regressions or breaking
    changes.
  • X.Y.Z - A specific stable release

Ports

All standard ports, as described in maddy docs.

  • 25 - SMTP inbound port.
  • 465, 587 - SMTP Submission ports
  • 993, 143 - IMAP4 ports

Volumes

/data - maddy state directory. Databases, queues, etc are stored here. You
might want to mount a named volume there. The main configuration file is stored
here too (/data/maddy.conf).

Management utility

To run management commands, create a temporary container with the same
/data directory and put the command after the image name, like this:

text
docker run --rm -it -v maddydata:/data foxcpp/maddy:0.7 creds create [email protected]
docker run --rm -it -v maddydata:/data foxcpp/maddy:0.7 imap-acct create [email protected]

Use the same image version as the running server. Things may break badly
otherwise.

Note that, if you modify messages using maddy subcommands while the server is running -
you must ensure that /tmp from the server is accessible for the management
command. One way to it is to run it using docker exec instead of docker run:

text
docker exec -it container_name_here maddy creds create [email protected]

Build Tags

Some Maddy features (such as automatic certificate management via ACME with a non-default libdns provider) require build tags to be passed to Maddy's build.sh, as this is run in the Dockerfile you must compile your own Docker image. Build tags can be set via the docker build argument ADDITIONAL_BUILD_TAGS e.g. docker build --build-arg ADDITIONAL_BUILD_TAGS="libdns_acmedns libdns_route53" -t yourorgname/maddy:yourtagname ..

TL;DR

text
docker volume create maddydata
docker run \
  --name maddy \
  -e MADDY_HOSTNAME=mx.maddy.test \
  -e MADDY_DOMAIN=maddy.test \
  -v maddydata:/data \
  -p 25:25 \
  -p 143:143 \
  -p 465:465 \
  -p 587:587 \
  -p 993:993 \
  foxcpp/maddy:0.7

It will fail on first startup. Copy TLS certificate to /data/tls/fullchain.pem
and key to /data/tls/privkey.pem. Run the server again. Finish DNS configuration
(DKIM keys, etc) as described in tutorials/setting-up/.


Faq (`docs/faq.md`)

Frequently Asked Questions

Unfortunately, GMail policies are opaque so we cannot tell why this happens.

Verify that you have a rDNS record set for the IP used
by sender server. Also some IPs may just happen to
have bad reputation - check it with various DNSBLs. In this
case you do not have much of a choice but to replace it.

Additionally, you may try marking multiple messages sent from
your domain as "not spam" in GMail UI.

Message sending fails with `dial tcp X.X.X.X:25: connect: connection timed out` in log

Your provider is blocking outbound SMTP traffic on port 25.

You either have to ask them to unblock it or forward
all outbound messages via a "smart-host".

What is resource usage of maddy?

For a small personal server, you do not need much more than a
single 1 GiB of RAM and disk space.

How to setup a catchall address?

https://github.com/foxcpp/maddy/issues/243#issuecomment-655694512

maddy command prints a "permission denied" error

Run maddy command under the same user as maddy itself.
E.g.

text
sudo -u maddy maddy creds ...

How maddy compares to MailCow or Mail-In-The-Box?

MailCow and MIAB are bundles of well-known email-related software configured to
work together. maddy is a single piece of software implementing subset of what
MailCow and MIAB offer.

maddy offers more uniform configuration system, more lightweight implementation
and has no dependency on Docker or similar technologies for deployment.

maddy may have more bugs than 20 years old battle-tested software.

It is easier to get help with MailCow/MITB since underlying implementations
are well-understood and have active community.

maddy has no Web interface for administration, that is currently done via CLI
utility.

How maddy IMAP server compares to WildDuck?

Both are "more secure by definition": root access is not required,
implementation is in memory-safe language, etc.

Both support message compression.

Both have first-class Unicode/internationalization support.

WildDuck may offer easier scalability options. maddy does not require you to
setup MongoDB and Redis servers, though. In fact, maddy in its default
configuration has no dependencies besides libc.

maddy has less builtin authentication providers. This means no
app-specific passwords and all that WildDuck lists under point 4 on their
features page.

maddy currently has no admin Web interface, all necessary DB changes are
performed via CLI utility.

How maddy SMTP server compares to ZoneMTA?

maddy SMTP server has a lot of similarities to ZoneMTA.
Both have powerful mechanisms for message routing (although designed
differently).

maddy does not require MongoDB server for deployment.

maddy has no web interface for queue inspection. However, it can
easily inspected by looking at files in /var/lib/maddy.

ZoneMTA has a number of features that may make it easier to integrate
with HTTP-based services. maddy speaks standard email protocols (SMTP,
Submission).

Is there a webmail?

No, at least currently.

I suggest you to check out alps if you
are fine with alpha-quality but extremely easy to deploy webmail.

Is there a content filter (spam filter)?

No. maddy moves email messages around, it does not classify
them as bad or good with the notable exception of sender policies.

It is possible to integrate rspamd using 'rspamd' module. Just add
rspamd line to checks in local_routing, it should just work
in most cases.

Is it production-ready?

maddy is considered "beta" quality. Several people use it for personal email.

Single process makes it unreliable. This is dumb!

This is a compromise between ease of management and reliability. Several
measures are implemented in code base in attempt to reduce possible effect
of bugs in one component.

Besides, you are not required to use a single process, it is easy to launch
maddy with a non-default configuration path and connect multiple instances
together using off-the-shelf protocols.


Multiple Domains (`docs/multiple-domains.md`)

Multiple domains configuration

By default, maddy uses email addresses as account identifiers for both
authentication and storage purposes. Therefore, account named [email protected]
is completely independent from [email protected]. They must be created
separately, may have different credentials and have separate IMAP mailboxes.

This makes it extremely easy to setup maddy to manage multiple otherwise
independent domains.

Default configuration file contains two macros - $(primary_domain) and
$(local_domains). They are used to used in several places thorough the
file to configure message routing, security checks, etc.

In general, you should just add all domains you want maddy to manage to
$(local_domains), like this:

text
$(primary_domain) = example.org
$(local_domains) = $(primary_domain) example.com

Note that you need to pick one domain as a "primary" for use in
auto-generated messages.

With that done, you can create accounts using both domains in the name, send
and receive messages and so on. Do not forget to configure corresponding SPF,
DMARC and MTA-STS records as was recommended in
the introduction tutorial.

Also note that you do not really need a separate TLS certificate for each
managed domain. You can have one hostname e.g. mail.example.org set as an MX
record for multiple domains.

If you want multiple domains to share username namespace, you should change
several more options.

You can make "[email protected]" and "[email protected]" users share the same
credentials of user "user" but have different IMAP mailboxes ("[email protected]"
and "[email protected]" correspondingly). For that, it is enough to set auth_map
globally to use email_localpart table:

text
auth_map email_localpart

This way, when user logs in as "[email protected]", "user" will be passed
to the authentication provider, but "[email protected]" will be passed to the
storage backend. You should create accounts like this:

text
maddy creds create user
maddy imap-acct create [email protected]
maddy imap-acct create [email protected]

If you want accounts to also share the same IMAP storage of account named
"user"
, you can set storage_map in IMAP endpoint and delivery_map in
storage backend to use email_locapart:

text
storage.imapsql local_mailboxes {
   ...
   delivery_map email_localpart # deliver "user@*" to "user"
}
imap tls://0.0.0.0:993 {
   ...
   storage &local_mailboxes
   ...
   storage_map email_localpart # "user@*" accesses "user" mailbox
}

You also might want to make it possible to log in without
specifying a domain at all. In this case, use email_localpart_optional for
both auth_map and storage_map.

You also need to make authorize_sender check (used in submission endpoint)
accept non-email usernames:

text
authorize_sender {
  ...
  user_to_email chain {
    step email_localpart_optional           # remove domain from username if present
    step email_with_domain $(local_domains) # expand username with all allowed domains
  }
}

TL;DR

Your options:

"[email protected]" and "[email protected]" have distinct credentials and
distinct mailboxes.

text
$(primary_domain) = example.org
$(local_domains) = example.org example.com

Create accounts as:

shell
maddy creds create [email protected]
maddy imap-acct create [email protected]
maddy creds create [email protected]
maddy imap-acct create [email protected]

"[email protected]" and "[email protected]" have same credentials but
distinct mailboxes.

text
$(primary_domain) = example.org
$(local_domains) = example.org example.com
auth_map email_localpart

Create accounts as:

shell
maddy creds create user
maddy imap-acct create [email protected]
maddy imap-acct create [email protected]

"[email protected]", "[email protected]", "user" have same credentials and same
mailboxes.

text
$(primary_domain) = example.org
   $(local_domains) = example.org example.com
   auth_map email_localpart_optional # authenticating as "user@*" checks credentials for "user"

   storage.imapsql local_mailboxes {
      ...
      delivery_map email_localpart_optional # deliver "user@*" to "user" mailbox
   }

   imap tls://0.0.0.0:993 {
      ...
      storage_map email_localpart_optional # authenticating as "user@*" accesses "user" mailboxes
   }

   submission tls://0.0.0.0:465 {
      check {
        authorize_sender {
          ...
          user_to_email chain {
            step email_localpart_optional           # remove domain from username if present
            step email_with_domain $(local_domains) # expand username with all allowed domains
          }
        }
      }
      ...
   }

Create accounts as:

shell
maddy creds create user
maddy imap-acct create user

Seclevels (`docs/seclevels.md`)

Outbound delivery security

maddy implements a number of schemes and protocols for discovery and
enforcement of security features supported by the recipient MTA.

Introduction to the problems of secure SMTP

Outbound delivery security involves two independent problems:

  • MX record authentication
  • TLS enforcement

MX record authentication

When MTA wants to deliver a message to a mailbox at remote domain, it needs to
discover the server to use for it. It is done through the lookup of DNS MX
records for the recipient.

Problem arises from the fact that DNS does not have any cryptographic
protection and so any malicious actor can technically modify the response to
contain any server. And MTA would use that server!

There are two protocols that solve this problem: MTA-STS and DNSSEC.
Former requires the MTA to verify used records against a list of rules published
via HTTPS. Later cryptographically signs the records themselves.

TLS enforcement

By default, server-server SMTP is unencrypted. If remote server supports TLS,
it is advertised via the ESMTP extension named STARTTLS, but malicious actor
controlling communication channel can hide the support for STARTTLS and sender
MTA will have to use plaintext. There needs to be a out-of-band authenticated
channel to indicate TLS support (and to require its use).

MTA-STS and DANE solve this problem. In the first case, if policy is in
"enforce" mode then MTA is required to use TLS when delivering messages to a
remote server. DANE does pretty much the same thing, but using DNSSEC-signed
TLSA records.

maddy policy details

maddy defines two values indicating how "secure" delivery of message will be:

  • MX security level
  • TLS security level

These values correspond to the problems described above. On delivery, the
established connection to the remote server is "ranked" using these values and
then they are compared against a number of policies (including local
configuration). If the effective value is lower than the required one, the
connection is closed and next candidate server is used. If all connections fail
this way - the delivery is failed (or deferred if there was a temporary error
when checking policies).

Below is the table summarizing the security level values defined in maddy and
protection they offer.

MX/TLS level None Encrypted Authenticated
None - P P
MTA-STS - P PA (see note 1)
DNSSEC - P PA

Legend: P - protects against passive attacks; A - protects against active
attacks

  • MX level: None. MX candidate was returned as a result of DNS lookup for the
    recipient domain, no additional checks done.

  • MX level: MTA-STS. Used MX matches the MTA-STS policy published by the
    recipient domain (even one in testing mode).

  • MX level: DNSSEC. MX record is signed.

  • TLS level: None. Plaintext connection was established, TLS is not available
    or failed.

  • TLS level: Encrypted. TLS connection was established, the server certificate
    failed X.509 and DANE verification.

  • TLS level: Authenticated. TLS connection was established, the server
    certificate passes X.509 or DANE verification.

Note 1: Persistent attacker able to control network connection can
interfere with policy refresh, downgrading protection to be secure only against
passive attacks.

maddy security policies

See Remote MX delivery for description of configuration options available for each policy mechanism
supported by maddy.


Upgrading (`docs/upgrading.md`)

Upgrading from older maddy versions

It is generally possible to just install latest version (e.g. using build.sh
script) over the existing installation.

It is recommended to backup state directory (usually /var/lib/maddy for Linux)
before doing so. The new server version may automatically convert DB files in a
way that will make them unreadable by older versions.

Specific instructions for upgrading between versions with incompatible changes
are documented on this page below.

Incompatible version migration

0.2 -> 0.3

0.3 includes a significant change to the authentication code that makes it
completely independent of IMAP index. This means 0.2 "unified" database cannot
be used in 0.3 and auto-migration is not possible. Additionally, the way
passwords are hashed is changed, meaning that after migration passwords will
need to be reset.

Migration utility is SQLite-specific, if you need one that works for
Postgres - reach out at the IRC channel.

  1. Make sure the server is not running.
text
systemctl stop maddy
  1. Take a backup of imapsql.db* files in state directory (/var/lib/maddy).
text
mkdir backup
cp /var/lib/maddy/imapsql.db* backup/
  1. Compile migration utility:
text
git clone https://github.com/foxcpp/maddy.git
cd maddy/
git checkout v0.3.0
cd cmd/migrate-db-0.2
go build
  1. Run compiled binary:
text
./migrate-db-0.2 /var/lib/maddy/imapsql.db
  1. Open maddy.conf and make following changes:

Remove local_authdb name from imapsql configuration block:

text
imapsql local_mailboxes {
    driver sqlite3
    dsn imapsql.db
}

Add local_authdb configuration block using pass_table module:

text
pass_table local_authdb {
    table sql_table {
        driver sqlite3
        dsn credentials.db
        table_name passwords
    }
}
  1. Use maddy creds create ACCOUNT_NAME to add credentials to pass_table
    store.

  2. Start the server back.

text
systemctl start maddy

0.1 -> 0.2

0.2 requires several changes in configuration file.

Change

text
sql local_mailboxes local_authdb {

to

text
imapsql local_mailboxes local_authdb {

Replace

text
replace_rcpt postmaster postmaster@$(primary_domain)

with

text
replace_rcpt static {
    entry postmaster postmaster@$(primary_domain)
}

and

text
replace_rcpt "(.+)\+(.+)@(.+)" "$1@$3"

with

text
replace_rcpt regexp "(.+)\+(.+)@(.+)" "$1@$3"

Internals Quirks (`docs/internals/quirks.md`)

Implementation quirks

This page documents unusual behavior of the maddy protocols implementations.
Some of these problems break standards, some don't but still can hurt
interoperability.

SMTP

  • for field is never included in the Received header field.

    This is allowed by RFC 2821.

IMAP

`sql`

  • \Recent flag is not reset in all cases.

    This does not break RFC 3501. Clients relying on it will work (much) less
    efficiently.


Internals Specifications (`docs/internals/specifications.md`)

Followed specifications

This page lists Internet Standards and other specifications followed by
maddy along with any known deviations.

Message format

  • RFC 2822 - Internet Message Format

  • RFC 2045 - Multipurpose Internet Mail Extensions (MIME) (part 1)

  • RFC 2046 - Multipurpose Internet Mail Extensions (MIME) (part 2)

  • RFC 2047 - Multipurpose Internet Mail Extensions (MIME) (part 3)

  • RFC 2048 - Multipurpose Internet Mail Extensions (MIME) (part 4)

  • RFC 2049 - Multipurpose Internet Mail Extensions (MIME) (part 5)

  • RFC 6532 - Internationalized Email Headers

  • RFC 2183 - Communicating Presentation Information in Internet Messages: The
    Content-Disposition Header Field

IMAP

  • RFC 3501 - Internet Message Access Protocol - Version 4rev1
    • Partial: \Recent flag is not reset sometimes.
  • RFC 2152 - UTF-7

Extensions

  • RFC 2595 - Using TLS with IMAP, POP3 and ACAP
  • RFC 7889 - The IMAP APPENDLIMIT Extension
  • RFC 3348 - The Internet Message Action Protocol (IMAP4). Child Mailbox
    Extension
  • RFC 6851 - Internet Message Access Protocol (IMAP) - MOVE Extension
  • RFC 6154 - IMAP LIST Extension for Special-Use Mailboxes
    • Partial: Only SPECIAL-USE capability.
  • RFC 5255 - Internet Message Access Protocol Internationalization
    • Partial: Only I18NLEVEL=1 capability.
  • RFC 4978 - The IMAP COMPRESS Extension
  • RFC 3691 - Internet Message Access Protocol (IMAP) UNSELECT command
  • RFC 2177 - IMAP4 IDLE command
  • RFC 7888 - IMAP4 Non-Synchronizing Literals
    • LITERAL+ capability.
  • RFC 4959 - IMAP Extension for Simple Authentication and Security Layer
    (SASL) Initial Client Response

SMTP

  • RFC 2033 - Local Mail Transfer Protocol
  • RFC 5321 - Simple Mail Transfer Protocol
  • RFC 6409 - Message Submission for Mail

Extensions

  • RFC 1870 - SMTP Service Extension for Message Size Declaration
  • RFC 2920 - SMTP Service Extension for Command Pipelining
    • Server support only, not used by SMTP client
  • RFC 2034 - SMTP Service Extension for Returning Enhanced Error Codes
  • RFC 3207 - SMTP Service Extension for Secure SMTP over Transport Layer
    Security
  • RFC 4954 - SMTP Service Extension for Authentication
  • RFC 6152 - SMTP Extension for 8-bit MIME
  • RFC 6531 - SMTP Extension for Internationalized Email

Misc

  • RFC 6522 - The Multipart/Report Content Type for the Reporting of Mail
    System Administrative Messages
  • RFC 3464 - An Extensible Message Format for Delivery Status Notifications
  • RFC 6533 - Internationalized Delivery Status and Disposition Notifications

Email security

User authentication

  • RFC 4422 - Simple Authentication and Security Layer (SASL)
  • RFC 4616 - The PLAIN Simple Authentication and Security Layer (SASL)
    Mechanism

Sender authentication

  • RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures
  • RFC 7001 - Message Header Field for Indicating Message Authentication Status
  • RFC 7208 - Sender Policy Framework (SPF) for Authorizing Use of Domains in
    Email, Version 1
  • RFC 7372 - Email Authentication Status Codes
  • RFC 7479 - Domain-based Message Authentication, Reporting, and Conformance
    (DMARC)
    • Partial: No report generation.
  • RFC 8301 - Cryptographic Algorithm and Key Usage Update to DomainKeys
    Identified Mail (DKIM)
  • RFC 8463 - A New Cryptographic Signature Method for DomainKeys Identified
    Mail (DKIM)
  • RFC 8616 - Email Authentication for Internationalized Mail

Recipient authentication

  • RFC 4033 - DNS Security Introduction and Requirements
  • RFC 6698 - The DNS-Based Authentication of Named Entities (DANE) Transport
    Layer Security (TLS) Protocol: TLSA
  • RFC 7672 - SMTP Security via Opportunistic DNS-Based Authentication of
    Named Entities (DANE) Transport Layer Security (TLS)
  • RFC 8461 - SMTP MTA Strict Transport Security (MTA-STS)

Unicode, encodings, internationalization

  • RFC 3492 - Punycode: A Bootstring encoding of Unicode for Internationalized
    Domain Names in Applications (IDNA)
  • RFC 3629 - UTF-8, a transformation format of ISO 10646
  • RFC 5890 - Internationalized Domain Names for Applications (IDNA):
    Definitions and Document Framework
  • RFC 5891 - Internationalized Domain Names for Applications (IDNA): Protocol
  • RFC 7616 - Preparation, Enforcement, and Comparison of Internationalized
    Strings Representing Usernames and Passwords
  • RFC 8264 - PRECIS Framework: Preparation, Enforcement, and Comparison of
    Internationalized Strings in Application Protocols
  • Unicode 11.0.0
    • UAX #15 - Unicode Normalization Forms

There is a huge list of non-Unicode encodings supported by message parser used
for IMAP static cache and search. See Unicode support page for
details.

Misc

  • RFC 5782 - DNS Blacklists and Whitelists

Internals Sqlite (`docs/internals/sqlite.md`)

maddy & SQLite

SQLite is a perfect choice for small deployments because no additional
configuration is required to get started. It is recommended for cases when you
have less than 10 mail accounts.

Note: SQLite requires DB-wide locking for writing, it means that multiple
messages can't be accepted in parallel. This is not the case for server-based
RDBMS where maddy can accept multiple messages in parallel even for a single
mailbox.

WAL mode

maddy forces WAL journal mode for SQLite. This makes things reasonably fast and
reduces locking contention which may be important for a typical mail server.

maddy uses increased WAL autocheckpoint interval. This means that while
maintaining a high write throughput, maddy will have to stop for a bit (0.5-1
second) every time 78 MiB is written to the DB (with default configuration it
is 15 MiB).

Note that when moving the database around you need to move WAL journal (-wal)
and shared memory (-shm) files as well, otherwise some changes to the DB will
be lost.

Query planner statistics

maddy updates query planner statistics on shutdown and every 5 hours. It
provides query planner with information to access the database in more
efficient way because go-imap-sql schema does use a few so called "low-quality
indexes".

Auto-vacuum

maddy turns on SQLite auto-vacuum feature. This means that database file size
will shrink when data is removed (compared to default when it remains unused).

Manual vacuuming

Auto-vacuuming can lead to database fragmentation and thus reduce the read
performance. To do manual vacuum operation to repack and defragment database
file, install the SQLite3 console utility and run the following commands:

text
sqlite3 -cmd 'vacuum' database_file_path_here.db
sqlite3 -cmd 'pragma wal_checkpoint(truncate)' database_file_path_here.db

It will take some time to complete, you can close the utility when the
sqlite> prompt appears.


Internals Unicode (`docs/internals/unicode.md`)

Unicode support

maddy has the first-class Unicode support in all components (modules). You do
not have to take any actions to make it work with internationalized domains,
mailbox names or non-ASCII message headers.

Internally, all text fields in maddy are represented in UTF-8 and handled using
Unicode-aware operations for comparisons, case-folding and so on.

Non-ASCII data in message headers and bodies

maddy SMTP implementation does not care about encodings used in MIME headers or
in Content-Type text/* charset field.

However, local IMAP storage implementation needs to perform certain operations
on header contents. This is mostly about SEARCH functionality. For IMAP search
to work correctly, the message body and headers should use one of the following
encodings:

  • ASCII
  • UTF-8
  • ISO-8859-1, 2, 3, 4, 9, 10, 13, 14, 15 or 16
  • Windows-1250, 1251 or 1252 (aka Code Page 1250 and so on)
  • KOI8-R
  • HZGB2312, GB18030
  • GBK (aka Code Page 936)
  • Shift JIS (aka Code Page 932 or Windows-31J)
  • Big-5 (aka Code Page 950)
  • EUC-JP
  • ISO-2022-JP

Support for HZGB2312 is currently disabled due to bugs with security
implications.

If mailbox includes a message with any encoding not listed here, it will not
be returned in search results for any request.

Behavior regarding handling of non-Unicode encodings is not considered stable
and may change between versions (including removal of supported encodings). If
you need your stuff to work correctly - start using UTF-8.

Configuration files

maddy configuration files are assumed to be encoded in UTF-8. Use of any other
encoding will break stuff, do not do it.

Domain names (e.g. in hostname directive or pipeline rules) can be represented
using the ACE form (aka Punycode). They will be converted to the Unicode form
internally.

Local credentials

'sql' storage backend and authentication provider enforce a number of additional
constraints on used account names.

PRECIS UsernameCaseMapped profile is enforced for local email addresses.
It limits the use of control and Bidi characters to make sure the used value
can be represented consistently in a variety of contexts. On top of that, the
address is case-folded and normalized to the NFC form for consistent internal
handling.

PRECIS OpaqueString profile is enforced for passwords. Less strict rules are
applied here. Runs of Unicode whitespace characters are replaced with a single
ASCII space. NFC normalization is applied afterwards. If the resulting string
is empty - the password is not accepted.

Both profiles are defined in RFC 8265, consult it for details.

Protocol support

SMTPUTF8 extension

maddy SMTP implementation includes support for the SMTPUTF8 extension as
defined in RFC 6531.

This means maddy can handle internationalized mailbox and domain names in MAIL
FROM, RCPT TO commands both for outbound and inbound delivery.

maddy will not accept messages with non-ASCII envelope addresses unless
SMTPUTF8 support is requested. If a message with SMTPUTF8 flag set is forwarded
to a server without SMTPUTF8 support, delivery will fail unless it is possible
to represent envelope addresses in the ASCII form (only domains use Unicode and
they can be converted to Punycode). Contents of message body (and header) are
not considered and always accepted and sent as-is, no automatic downgrading or
reencoding is done.

IMAP UTF8, I18NLEVEL extensions

Currently, maddy does not include support for UTF8 and I18NLEVEL IMAP
extensions. However, it is not a problem that can prevent it from correctly
handling UTF-8 messages (or even messages in other non-ASCII encodings
mentioned above).

Clients that want to implement proper handling for Unicode strings may assume
maddy does not handle them properly in e.g. SEARCH commands and so such clients
may download messages and process them locally.


Reference Config Syntax (`docs/reference/config-syntax.md`)

Configuration files syntax

Note: This file is a technical document describing how
maddy parses configuration files.

Configuration consists of newline-delimited "directives". Each directive can
have zero or more arguments.

text
directive0
directive1 arg0 arg1

Any line starting with # is ignored. Empty lines are ignored too.

Quoting

Strings with whitespace should be wrapped into double quotes to make sure they
will be interpreted as a single argument.

text
directive0 two arguments
directive1 "one argument"

String wrapped in quotes may contain newlines and they will not be interpreted
as a directive separator.

text
directive0 "one long big
argument for directive0"

Quotes and only quotes can be escaped inside literals: \"

Backslash can be used at the end of line to continue the directve on the next
line.

Blocks

A directive may have several subdirectives. They are written in a {-enclosed
block like this:

text
directive0 arg0 arg1 {
    subdirective0 arg0 arg1
    subdirective1 etc
}

Subdirectives can have blocks too.

text
directive0 {
    subdirective0 {
        subdirective2 {
            a
            b
            c
        }
    }
    subdirective1 { }
}

Level of nesting is limited, but you should never hit the limit with correct
configuration.

In most cases, an empty block is equivalent to no block:

text
directive { }
directive2 # same as above

Environment variables

Environment variables can be referenced in the configuration using either
{env:VARIABLENAME} syntax.

Non-existent variables are expanded to empty strings and not removed from
the arguments list. In the following example, directive0 will have one argument
independently of whether VAR is defined.

text
directive0 {env:VAR}

Parse is forgiving and incomplete variable placeholder (e.g. '{env:VAR') will
be left as-is. Variables are expanded inside quotes too.

Snippets & imports

You can reuse blocks of configuration by defining them as "snippets". Snippet
is just a directive with a block, declared tp top level (not inside any blocks)
and with a directive name wrapped in curly braces.

text
(snippetname) {
    a
    b
    c
}

The snippet can then be referenced using 'import' meta-directive.

text
unrelated0
unrelated1
import snippetname

The above example will be expanded into the following configuration:

text
unrelated0
unrelated1
a
b
c

Import statement also can be used to include content from other files. It works
exactly the same way as with snippets but the file path should be used instead.
The path can be either relative to the location of the currently processed
configuration file or absolute. If there are both snippet and file with the
same name - snippet will be used.

text
# /etc/maddy/tls.conf
tls long_path_to_certificate long_path_to_private_key

# /etc/maddy/maddy.conf
smtp tcp://0.0.0.0:25 {
    import tls.conf
}
text
# Expanded into:
smtp tcp://0.0.0.0:25 {
    tls long_path_to_certificate long_path_to_private_key
}

The imported file can introduce new snippets and they can be referenced in any
processed configuration file.

Duration values

Directives that accept duration use the following format: A sequence of decimal
digits with an optional fraction and unit suffix (zero can be specified without
a suffix). If multiple values are specified, they will be added.

Valid unit suffixes: "h" (hours), "m" (minutes), "s" (seconds), "ms" (milliseconds).
Implementation also accepts us and ns for microseconds and nanoseconds, but these
values are useless in practice.

Examples:

text
1h
1h 5m
1h5m
0

Data size values

Similar to duration values, but fractions are not allowed and suffixes are different.

Valid unit suffixes: "G" (gibibyte, 1024^3 bytes), "M" (mebibyte, 1024^2 bytes),
"K" (kibibyte, 1024 bytes), "B" or "b" (byte).

Examples:

text
32M
3M 5K
5b

Also note that the following is not valid, unlike Duration values syntax:

text
32M5K

Address Definitions

Maddy configuration uses URL-like syntax to specify network addresses.

  • unix://file_path – Unix domain socket. Relative paths are relative to runtime directory (/run/maddy).
  • tcp://ADDRESS:PORT – TCP/IP socket.
  • tls://ADDRESS:PORT – TCP/IP socket using TLS.

Dummy Module

No-op module. It doesn't need to be configured explicitly and can be referenced
using "dummy" name. It can act as a delivery target or auth.
provider. In the latter case, it will accept any credentials, allowing any
client to authenticate using any username and password (use with care!).


Reference Global Config (`docs/reference/global-config.md`)

Global configuration directives

These directives can be specified outside of any
configuration blocks and they are applied to all modules.

Some directives can be overridden on per-module basis (e.g. hostname).

state_dir _path_

Default: /var/lib/maddy

The path to the state directory. This directory will be used to store all
persistent data and should be writable.


runtime_dir _path_

Default: /run/maddy

The path to the runtime directory. Used for Unix sockets and other temporary
objects. Should be writable.


hostname _domain_

Default: not specified

Internet hostname of this mail server. Typicall FQDN is used. It is recommended
to make sure domain specified here resolved to the public IP of the server.


auth_map _module-reference_

Default: identity

Use the specified table to translate SASL usernames before passing it to the
authentication provider.

Before username is looked up, it is normalized using function defined by
auth_map_normalize.

Note that auth_map does not affect the storage account name used. You probably
should also use storage_map in IMAP config block to handle this.

This directive is useful if used authentication provider does not support
using emails as usernames but you still want users to have separate mailboxes
on separate domains. In this case, use it with email_localpart table:

text
auth_map email_localpart

With this configuration, [email protected] and [email protected] will use
user credentials when authenticating, but will access [email protected] and
[email protected] mailboxes correspondingly. If you want to also accept
user as a username, use auth_map email_localpart_optional.

If you want [email protected] and [email protected] to have the same mailbox,
also set storage_map in IMAP config block to use email_localpart
(or email_localpart_optional if you want to also accept just "user"):

text
storage_map email_localpart

In this case you will need to create storage accounts without domain part in
the name:

text
maddy imap-acct create user # instead of [email protected]

auth_map_normalize _function_

Default: auto

Normalization function to apply to SASL usernames before mapping
them to storage accounts.

Available options:

  • auto precis_casefold_email for valid emails, precis_casefold otherwise.
  • precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain
  • precis_casefold PRECIS UsernameCaseMapped profile for the entire string
  • precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain
  • precis PRECIS UsernameCasePreserved profile for the entire string
  • casefold Convert to lower case
  • noop Nothing

autogenerated_msg_domain _domain_

Default: not specified

Domain that is used in From field for auto-generated messages (such as Delivery
Status Notifications).


tls `file` _cert-file_ _pkey-file_ | _module-reference_ | `off`

Default: not specified

Default TLS certificate to use for all endpoints.

Must be present in either all endpoint modules configuration blocks or as
global directive.

You can also specify other configuration options such as cipher suites and TLS
version. See maddy-tls(5) for details. maddy uses reasonable
cipher suites and TLS versions by default so you generally don't have to worry
about it.


tls_client { ... }

Default: not specified

This is optional block that specifies various TLS-related options to use when
making outbound connections. See TLS client configuration for details on
directives that can be used in it. maddy uses reasonable cipher suites and TLS
versions by default so you generally don't have to worry about it.


log _targets..._ | `off`

Default: stderr

Write log to one of more "targets".

The target can be one or the following:

  • stderr – Write logs to stderr.
  • stderr_ts – Write logs to stderr with timestamps.
  • syslog – Send logs to the local syslog daemon.
  • file path – Write (append) logs to file.

Example:

text
log syslog /var/log/maddy.log

Note: Maddy does not perform log files rotation, this is the job of the
logrotate daemon. Send SIGUSR1 to maddy process to make it reopen log files.


debug _boolean_

Default: no

Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.


Reference Modules (`docs/reference/modules.md`)

Modules introduction

maddy is built of many small components called "modules". Each module does one
certain well-defined task. Modules can be connected to each other in arbitrary
ways to achieve wanted functionality. Default configuration file defines
set of modules that together implement typical email server stack.

To specify the module that should be used by another module for something, look
for configuration directives with "module reference" argument. Then
put the module name as an argument for it. Optionally, if referenced module
needs that, put additional arguments after the name. You can also put a
configuration block with additional directives specifing the module
configuration.

Here are some examples:

text
smtp ... {
    # Deliver messages to the 'dummy' module with the default configuration.
    deliver_to dummy

    # Deliver messages to the 'target.smtp' module with
    # 'tcp://127.0.0.1:1125' argument as a configuration.
    deliver_to smtp tcp://127.0.0.1:1125

    # Deliver messages to the 'queue' module with the specified configuration.
    deliver_to queue {
        target ...
        max_tries 10
    }
}

Additionally, module configuration can be placed in a separate named block
at the top-level and referenced by its name where it is needed.

Here is the example:

text
storage.imapsql local_mailboxes {
    driver sqlite3
    dsn all.db
}

smtp ... {
    deliver_to &local_mailboxes
}

It is recommended to use this syntax for modules that are 'expensive' to
initialize such as storage backends and authentication providers.

For top-level configuration block definition, syntax is as follows:

text
namespace.module_name config_block_name... {
    module_configuration
}

If config_block_name is omitted, it will be the same as module_name. Multiple
names can be specified. All names must be unique.

Note the "storage." prefix. This is the actual module name and includes
"namespace". It is a little cheating to make more concise names and can
be omitted when you reference the module where it is used since it can
be implied (e.g. putting module reference in "check{}" likely means you want
something with "check." prefix)

Usual module arguments can't be specified when using this syntax, however,
modules usually provide explicit directives that allow to specify the needed
values. For example 'sql sqlite3 all.db' is equivalent to

text
storage.imapsql {
    driver sqlite3
    dsn all.db
}

Reference Smtp Pipeline (`docs/reference/smtp-pipeline.md`)

SMTP message routing (pipeline)

Message pipeline

A message pipeline is a set of module references and associated rules that
describe how to handle messages.

The pipeline is responsible for

  • Running message filters (called "checks"), (e.g. DKIM signature verification,
    DNSBL lookup, and so on).
  • Running message modifiers (e.g. DKIM signature creation).
  • Associating each message recipient with one or more delivery targets.
    Delivery target is a module that does the final processing (delivery) of the
    message.

Message handling flow is as follows:

  • Execute checks referenced in top-level check blocks (if any)
  • Execute modifiers referenced in top-level modify blocks (if any)
  • If there are source blocks - select one that matches the message sender (as
    specified in MAIL FROM). If there are no source blocks - the entire
    configuration is assumed to be the default_source block.
  • Execute checks referenced in check blocks inside the selected source block
    (if any).
  • Execute modifiers referenced in modify blocks inside selected source
    block (if any).

Then, for each recipient:

  • Select the destination block that matches it. If there are
    no destination blocks - the entire used source block is interpreted as if it
    was a default_destination block.
  • Execute checks referenced in the check block inside the selected destination
    block (if any).
  • Execute modifiers referenced in modify block inside the selected destination
    block (if any).
  • If the used block contains the reject directive - reject the recipient with
    the specified SMTP status code.
  • If the used block contains the deliver_to directive - pass the message to the
    specified target module. Only recipients that are handled
    by the used block are visible to the target.

Each recipient is handled only by a single destination block, in case of
overlapping destination - the first one takes priority.

text
destination example.org {
    deliver_to targetA
}
destination example.org { # ambiguous and thus not allowed
    deliver_to targetB
}

Same goes for source blocks, each message is handled only by a single block.

Each recipient block should contain at least one deliver_to directive or
reject directive. If destination blocks are used, then
default_destination block should also be used to specify behavior for
unmatched recipients. Same goes for source blocks, default_source should be
used if source is used.

That is, pipeline configuration should explicitly specify behavior for each
possible sender/recipient combination.

Additionally, directives that specify final handling decision (deliver_to,
reject) can't be used at the same level as source/destination rules.
Consider example:

text
destination example.org {
    deliver_to local_mboxes
}
reject

It is not obvious whether reject applies to all recipients or
just for non-example.org ones, hence this is not allowed.

Complete configuration example using all of the mentioned directives:

text
check {
    # Run a check to make sure source SMTP server identification
    # is legit.
    spf
}

# Messages coming from senders at example.org will be handled in
# accordance with the following configuration block.
source example.org {
    # We are example.com, so deliver all messages with recipients
    # at example.com to our local mailboxes.
    destination example.com {
        deliver_to &local_mailboxes
    }

    # We don't do anything with recipients at different domains
    # because we are not an open relay, thus we reject them.
    default_destination {
        reject 521 5.0.0 "User not local"
    }
}

# We do our business only with example.org, so reject all
# other senders.
default_source {
    reject
}

Directives

check _block name_ { ... }

Context: pipeline configuration, source block, destination block

List of the module references for checks that should be executed on
messages handled by block where 'check' is placed in.

Note that message body checks placed in destination block are currently
ignored. Due to the way SMTP protocol is defined, they would cause message to
be rejected for all recipients which is not what you usually want when using
such configurations.

Example:

text
check {
    # Reference implicitly defined default configuration for check.
    spf

    # Inline definition of custom config.
    spf {
         # Configuration for spf goes here.
         permerr_action reject
    }
}

It is also possible to define the block of checks at the top level
as "checks" module and reference it using & syntax. Example:

text
checks inbound_checks {
	spf
	dkim
}

# ... somewhere else ...
{
	...
	check &inbound_checks
}

modify { ... }

Default: not specified
Context: pipeline configuration, source block, destination block

List of the module references for modifiers that should be executed on
messages handled by block where 'modify' is placed in.

Message modifiers are similar to checks with the difference in that checks
purpose is to verify whether the message is legitimate and valid per local
policy, while modifier purpose is to post-process message and its metadata
before final delivery.

For example, modifier can replace recipient address to make message delivered
to the different mailbox or it can cryptographically sign outgoing message
(e.g. using DKIM). Some modifier can perform multiple unrelated modifications
on the message.

Note: Modifiers that affect source address can be used only globally or on
per-source basis, they will be no-op inside destination blocks. Modifiers that
affect the message header will affect it for all recipients.

It is also possible to define the block of modifiers at the top level
as "modiifers" module and reference it using & syntax. Example:

text
modifiers local_modifiers {
	replace_rcpt file /etc/maddy/aliases
}

# ... somewhere else ...
{
	...
	modify &local_modifiers
}

reject _smtp-code_ _smtp-enhanced-code_ _error-description_ reject _smtp-code_ _smtp-enhanced-code_ reject _smtp-code_ reject

Context: destination block

Messages handled by the configuration block with this directive will be
rejected with the specified SMTP error.

If you aren't sure which codes to use, use 541 and 5.4.0 with your message or
just leave all arguments out, the error description will say "message is
rejected due to policy reasons" which is usually what you want to mean.

reject can't be used in the same block with deliver_to or
destination/source directives.

Example:

text
reject 541 5.4.0 "We don't like example.org, go away"

deliver_to _target-config-block_

Context: pipeline configuration, source block, destination block

Deliver the message to the referenced delivery target. What happens next is
defined solely by used target. If deliver_to is used inside destination
block, only matching recipients will be passed to the target.


source_in _table-reference_ { ... }

Context: pipeline configuration

Handle messages with envelope senders present in the specified table in
accordance with the specified configuration block.

Takes precedence over all sender directives.

Example:

text
source_in file /etc/maddy/banned_addrs {
	reject 550 5.7.0 "You are not welcome here"
}
source example.org {
	...
}
...

See destination_in documentation for note about table configuration.


source _rules..._ { ... }

Context: pipeline configuration

Handle messages with MAIL FROM value (sender address) matching any of the rules
in accordance with the specified configuration block.

"Rule" is either a domain or a complete address. In case of overlapping
'rules', first one takes priority. Matching is case-insensitive.

Example:

text
# All messages coming from example.org domain will be delivered
# to local_mailboxes.
source example.org {
    deliver_to &local_mailboxes
}
# Messages coming from different domains will be rejected.
default_source {
    reject 521 5.0.0 "You were not invited"
}

reroute { ... }

Context: pipeline configuration, source block, destination block

This directive allows to make message routing decisions based on the
result of modifiers. The block can contain all pipeline directives and they
will be handled the same with the exception that source and destination rules
will use the final recipient and sender values (e.g. after all modifiers are
applied).

Here is the concrete example how it can be useful:

text
destination example.org {
    modify {
        replace_rcpt file /etc/maddy/aliases
    }
    reroute {
        destination example.org {
            deliver_to &local_mailboxes
        }
        default_destination {
            deliver_to &remote_queue
        }
    }
}

This configuration allows to specify alias local addresses to remote ones
without being an open relay, since remote_queue can be used only if remote
address was introduced as a result of rewrite of local address.

Warning: If you have DMARC enabled (default), results generated by SPF
and DKIM checks inside a reroute block will not be considered in DMARC
evaluation.


destination_in _table-reference_ { ... }

Context: pipeline configuration, source block

Handle messages with envelope recipients present in the specified table in
accordance with the specified configuration block.

Takes precedence over all 'destination' directives.

Example:

text
destination_in file /etc/maddy/remote_addrs {
	deliver_to smtp tcp://10.0.0.7:25
}
destination example.com {
	deliver_to &local_mailboxes
}
...

Note that due to the syntax restrictions, it is not possible to specify
extended configuration for table module. E.g. this is not valid:

text
destination_in sql_table {
	dsn ...
	driver ...
} {
	deliver_to whatever
}

In this case, configuration should be specified separately and be referneced
using '&' syntax:

text
table.sql_table remote_addrs {
	dsn ...
	driver ...
}

whatever {
	destination_in &remote_addrs {
		deliver_to whatever
	}
}

destination _rule..._ { ... }

Context: pipeline configuration, source block

Handle messages with RCPT TO value (recipient address) matching any of the
rules in accordance with the specified configuration block.

"Rule" is either a domain or a complete address. Duplicate rules are not
allowed. Matching is case-insensitive.

Note that messages with multiple recipients are split into multiple messages if
they have recipients matched by multiple blocks. Each block will see the
message only with recipients matched by its rules.

Example:

text
# Messages with recipients at example.com domain will be
# delivered to local_mailboxes target.
destination example.com {
    deliver_to &local_mailboxes
}

# Messages with other recipients will be rejected.
default_destination {
    rejected 541 5.0.0 "User not local"
}

Reusable pipeline snippets (msgpipeline module)

The message pipeline can be used independently of the SMTP module in other
contexts that require a delivery target via msgpipeline module.

Example:

text
msgpipeline local_routing {
    destination whatever.com {
        deliver_to dummy
    }
}

# ... somewhere else ...
deliver_to &local_routing

Reference Tls Acme (`docs/reference/tls-acme.md`)

Automatic certificate management via ACME

Maddy supports obtaining certificates using ACME protocol.

To use it, create a configuration name for tls.loader.acme
and reference it from endpoints that should use automatically
configured certificates:

text
tls.loader.acme local_tls {
    email [email protected]
    agreed # indicate your agreement with Let's Encrypt ToS
    challenge dns-01
}

smtp tcp://127.0.0.1:25 {
    tls &local_tls
    ...
}

You can also use a global tls directive to use automatically
obtained certificates for all endpoints:

text
tls {
    loader acme {
        email [email protected]
        agreed
        challenge dns-01
    }
}

Note: tls &local_tls as a global directive won't work because
global directives are initialized before other configuration blocks.

Currently the only supported challenge is dns-01 one therefore
you also need to configure the DNS provider:

text
tls.loader.acme local_tls {
    email [email protected]
    agreed
    challenge dns-01
    dns PROVIDER_NAME {
        ...
    }
}

See below for supported providers and necessary configuration
for each.

Configuration directives

text
tls.loader.acme {
    debug off
    hostname example.maddy.invalid
    store_path /var/lib/maddy/acme
    ca https://acme-v02.api.letsencrypt.org/directory
    test_ca https://acme-staging-v02.api.letsencrypt.org/directory
    email [email protected]
    agreed off
    challenge dns-01
    dns ...
}

debug _boolean_

Default: global directive value

Enable debug logging.


hostname _str_

Required.
Default: global directive value

Domain name to issue certificate for.


store_path _path_

Default: state_dir/acme

Where to store issued certificates and associated metadata.
Currently only filesystem-based store is supported.


ca _url_

Default: Let's Encrypt production CA

URL of ACME directory to use.


test_ca _url_

Default: Let's Encrypt staging CA

URL of ACME directory to use for retries should
primary CA fail.

maddy will keep attempting to issues certificates
using test_ca until it succeeds then it will switch
back to the one configured via 'ca' option.

This avoids rate limit issues with production CA.


override_domain _domain_

Default: not set

Override the domain to set the TXT record on for DNS-01 challenge.
This is to delegate the challenge to a different domain.

See https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation
for explanation why this might be useful.


email _str_

Default: not set

Email to pass while registering an ACME account.


agreed _boolean_

Default: false

Whether you agreed to ToS of the CA service you are using.


challenge `dns-01`

Default: not set

Challenge(s) to use while performing domain verification.

DNS providers

Support for some providers is not provided by standard builds.
To be able to use these, you need to compile maddy
with "libdns_PROVIDER" build tag.
E.g.

text
./build.sh --tags 'libdns_googleclouddns'
  • gandi
text
dns gandi {
    api_token "token"
}
  • digitalocean
text
dns digitalocean {
    api_token "..."
}
  • cloudflare

See https://github.com/libdns/cloudflare#authenticating

text
dns cloudflare {
    api_token "..."
}
  • vultr
text
dns vultr {
    api_token "..."
}
  • hetzner
text
dns hetzner {
    api_token "..."
}
  • namecheap
text
dns namecheap {
    api_key "..."
    api_username "..."

    # optional: API endpoint, production one is used if not set.
    endpoint "https://api.namecheap.com/xml.response"

    # optional: your public IP, discovered using icanhazip.com if not set
    client_ip 1.2.3.4
}
  • googleclouddns (non-default)
text
dns googleclouddns {
    project "project_id"
    service_account_json "path"
}
  • route53 (non-default)
text
dns route53 {
    secret_access_key "..."
    access_key_id "..."
    # or use environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
}
  • leaseweb (non-default)
text
dns leaseweb {
    api_key "key"
}
  • metaname (non-default)
text
dns metaname {
    api_key "key"
    account_ref "reference"
}
  • alidns (non-default)
text
dns alidns {
    key_id "..."
    key_secret "..."
}
  • namedotcom (non-default)
text
dns namedotcom {
    user "..."
    token "..."
}
  • rfc2136 (non-default)
text
dns rfc2136 {
    key_name "..."
    # Secret
    key "..."
    # HMAC algorithm used to generate the key, lowercase, e.g. hmac-sha512
    key_alg "..."
    # server to which the dynamic update will be sent, e.g. 127.0.0.1
    # you can also specify the port: 127.0.0.1:53
    server "..."
}
  • acmedns (non-default)
text
dns acmedns {
    username "..."
    password "..."
    subdomain "..."
    server_url "..."
}

Reference Tls (`docs/reference/tls.md`)

TLS configuration

Server-side

TLS certificates are obtained by modules called "certificate loaders". 'tls' directive
arguments specify name of loader to use and arguments. Due to syntax limitations
advanced configuration for loader should be specified using 'loader' directive, see
below.

text
tls file cert.pem key.pem {
	protocols tls1.2 tls1.3
	curves X25519
	ciphers ...
}

tls {
	loader file cert.pem key.pem {
		# Options for loader go here.
	}
	protocols tls1.2 tls1.3
	curves X25519
	ciphers ...
}

Available certificate loaders

  • file – Accepts argument pairs specifying certificate and then key.
    E.g. tls file certA.pem keyA.pem certB.pem keyB.pem.
    If multiple certificates are listed, SNI will be used.
  • acme – Automatically obtains a certificate using ACME protocol (Let's Encrypt)
  • off – Not really a loader but a special value for tls directive,
    explicitly disables TLS for endpoint(s).

Advanced TLS configuration

Note: maddy uses secure defaults and TLS handshake is resistant to active downgrade attacks. There is no need to change anything in most cases.


protocols _min-version_ _max-version_ | _version_

Default: tls1.0 tls1.3

Minimum/maximum accepted TLS version. If only one value is specified, it will
be the only one usable version.

Valid values are: tls1.0, tls1.1, tls1.2, tls1.3


ciphers _ciphers..._

Default: Go version-defined set of 'secure ciphers', ordered by hardware
performance

List of supported cipher suites, in preference order. Not used with TLS 1.3.

Valid values:

  • RSA-WITH-RC4128-SHA
  • RSA-WITH-3DES-EDE-CBC-SHA
  • RSA-WITH-AES128-CBC-SHA
  • RSA-WITH-AES256-CBC-SHA
  • RSA-WITH-AES128-CBC-SHA256
  • RSA-WITH-AES128-GCM-SHA256
  • RSA-WITH-AES256-GCM-SHA384
  • ECDHE-ECDSA-WITH-RC4128-SHA
  • ECDHE-ECDSA-WITH-AES128-CBC-SHA
  • ECDHE-ECDSA-WITH-AES256-CBC-SHA
  • ECDHE-RSA-WITH-RC4128-SHA
  • ECDHE-RSA-WITH-3DES-EDE-CBC-SHA
  • ECDHE-RSA-WITH-AES128-CBC-SHA
  • ECDHE-RSA-WITH-AES256-CBC-SHA
  • ECDHE-ECDSA-WITH-AES128-CBC-SHA256
  • ECDHE-RSA-WITH-AES128-CBC-SHA256
  • ECDHE-RSA-WITH-AES128-GCM-SHA256
  • ECDHE-ECDSA-WITH-AES128-GCM-SHA256
  • ECDHE-RSA-WITH-AES256-GCM-SHA384
  • ECDHE-ECDSA-WITH-AES256-GCM-SHA384
  • ECDHE-RSA-WITH-CHACHA20-POLY1305
  • ECDHE-ECDSA-WITH-CHACHA20-POLY1305

curves _curves..._

Default: defined by Go version

The elliptic curves that will be used in an ECDHE handshake, in preference
order.

Valid values: p256, p384, p521, X25519.

Client

tls_client directive allows to customize behavior of TLS client implementation,
notably adjusting minimal and maximal TLS versions and allowed cipher suites,
enabling TLS client authentication.

text
tls_client {
    protocols tls1.2 tls1.3
    ciphers ...
    curves X25519
    root_ca /etc/ssl/cert.pem

    cert /etc/ssl/private/maddy-client.pem
    key /etc/ssl/private/maddy-client.pem
}

protocols _min-version_ _max-version_ | _version_

Default: tls1.0 tls1.3

Minimum/maximum accepted TLS version. If only one value is specified, it will
be the only one usable version.

Valid values are: tls1.0, tls1.1, tls1.2, tls1.3


ciphers _ciphers..._

Default: Go version-defined set of 'secure ciphers', ordered by hardware
performance

List of supported cipher suites, in preference order. Not used with TLS 1.3.

See TLS server configuration for list of supported values.


curves _curves..._

Default: defined by Go version

The elliptic curves that will be used in an ECDHE handshake, in preference
order.

Valid values: p256, p384, p521, X25519.


root_ca _paths..._

Default: system CA pool

List of files with PEM-encoded CA certificates to use when verifying
server certificates.


cert _cert-path_ key _key-path_

Default: not specified

Present the specified certificate when server requests a client certificate.
Files should use PEM format. Both directives should be specified.


Third Party Dovecot (`docs/third-party/dovecot.md`)

Dovecot

Builtin maddy IMAP server may not match your requirements in terms of
performance, reliability or anything. For this reason it is possible to
integrate it with any external IMAP server that implements necessary
protocols. Here is how to do it for Dovecot.

  1. Get rid of imap endpoint and existing local_authdb and local_mailboxes
    blocks.

  2. Setup Dovecot to provide LMTP endpoint

Here is an example configuration snippet:

text
# /etc/dovecot/dovecot.conf
protocols = imap lmtp

# /etc/dovecot/conf.d/10-master.conf
service lmtp {
 unix_listener lmtp-maddy {
   mode = 0600
   user = maddy
  }
}

Add local_mailboxes block to maddy config using target.lmtp module:

text
target.lmtp local_mailboxes {
    targets unix:///var/run/dovecot/lmtp-maddy
}

Authentication

In addition to MTA service, maddy also provides Submission service, but it
needs authentication provider data to work correctly, maddy can use Dovecot
SASL authentication protocol for it.

You need the following in Dovecot's 10-master.conf:

text
service auth {
  unix_listener auth-maddy-client {
    mode = 0660
    user = maddy
  }
}

Then just configure dovecot_sasl module for submission:

text
submission ... {
    auth dovecot_sasl unix:///var/run/dovecot/auth-maddy-client
    ... other configuration ...
}

Other IMAP servers

Integration with other IMAP servers might be more problematic because there is
no standard protocol for authentication delegation. You might need to configure
the IMAP server to implement MSA functionality by forwarding messages to maddy
for outbound delivery. This might require more configuration changes on maddy
side since by default it will not allow relay on port 25 even for localhost
addresses. The easiest way is to create another SMTP endpoint on some port
(probably Submission port):

text
smtp tcp://127.0.0.1:587 {
    deliver_to &remote_queue
}

And configure IMAP server's Submission service to forward outbound messages
there.

Depending on how Submission service is implemented you may also need to route
messages for local domains back to it via LMTP:

text
smtp tcp://127.0.0.1:587 {
    destination postmaster $(local_domains) {
        deliver_to &local_routing
    }
    default_destination {
        deliver_to &remote_queue
    }
}

Third Party Mailman3 (`docs/third-party/mailman3.md`)

Mailman 3

Setting up Mailman 3 with maddy involves some additional work as compared to
other MTAs as there is no Python package in Mailman suite that can generate
address lists in format supported by maddy.

We assume you are already familiar with Mailman configuration guidelines and
how stuff works in general/for other MTAs.

Accepting messages

First of all, you need to use NullMTA package for mta.incoming so Mailman will
not try to generate any configs. LMTP listener is configured as usual.

text
[mta]
incoming: mailman.mta.null.NullMTA
lmtp_host: 127.0.0.1
lmtp_port: 8024

After that, you will need to configure maddy to send messages to Mailman.

The preferable way of doing so is destination_in and table.regexp:

text
msgpipeline local_routing {
    destination_in regexp "first-mailinglist(-(bounces\+.*|confirm\+.*|join|leave|owner|request|subscribe|unsubscribe))[email protected]" {
        deliver_to lmtp tcp://127.0.0.1:8024
    }
    destination_in regexp "second-mailinglist(-(bounces\+.*|confirm\+.*|join|leave|owner|request|subscribe|unsubscribe))[email protected]" {
        deliver_to lmtp tcp://127.0.0.1:8024
    }

    ...
}

A more simple option is also meaningful (provided you have a separate domain
for lists):

text
msgpipeline local_routing {
    destination lists.example.org {
        deliver_to lmtp tcp://127.0.0.1:8024
    }

    ...
}

But this variant will lead to inefficient handling of non-existing subaddresses.
See Mailman Core issue 14 for
details. (5 year old issue, sigh...)

Sending messages

It is recommended to configure Mailman to send messages using Submission port
with authentication and TLS as maddy does not allow relay on port 25 for local
clients as some MTAs do:

text
[mta]
# ... incoming configuration here ...
outgoing: mailman.mta.deliver.deliver
smtp_host: mx.example.org
smtp_port: 465
smtp_user: [email protected]
smtp_pass: something-very-secret
smtp_secure_mode: smtps

If you do not want to use TLS and/or authentication you can create a separate
endpoint and just point Mailman to it. E.g.

text
smtp tcp://127.0.0.1:2525 {
    destination postmaster $(local_domains) {
        deliver_to &local_routing
    }
    default_destination {
        deliver_to &remote_queue
    }
}

Note that if you use a separate domain for lists, it need to be included in
local_domains macro in default config. This will ensure maddy signs messages
using DKIM for outbound messages. It is also highly recommended to configure
ARC in Mailman 3.


Third Party Rspamd (`docs/third-party/rspamd.md`)

rspamd

maddy has direct support for rspamd HTTP protocol. There is no need to use
milter proxy.

If rspamd is running locally, it is enough to just add rspamd check
with default configuration into appropriate check block (probably in
local_routing):

text
check {
    ...
    rspamd
}

You might want to disable builtin SPF, DKIM and DMARC for performance
reasons but note that at the moment, maddy will not generate
Authentication-Results field with rspamd results.

If rspamd is not running on a local machine, change api_path to point
to the "normal" worker socket:

text
check {
    ...
    rspamd {
        api_path http://spam-check.example.org:11333
    }
}

Default mapping of rspamd action -> maddy action is as follows:

  • "add header" => Quarantine
  • "rewrite subject" => Quarantine
  • "soft reject" => Reject with temporary error
  • "reject" => Reject with permanent error
  • "greylist" => Ignored

Third Party Smtp Servers (`docs/third-party/smtp-servers.md`)

External SMTP server

It is possible to use maddy as an IMAP server only and have it interface with
external SMTP server using standard protocols.

Here is the minimal configuration that creates a local IMAP index, credentials
database and IMAP endpoint:

text
# Credentials DB.
table.pass_table local_authdb {
    table sql_table {
        driver sqlite3
        dsn credentials.db
        table_name passwords
    }
}

# IMAP storage/index.
storage.imapsql local_mailboxes {
    driver sqlite3
    dsn imapsql.db
}

# IMAP endpoint using these above.
imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
    auth &local_authdb
    storage &local_mailboxes
}

To accept local messages from an external SMTP server
it is possible to create an LMTP endpoint:

text
# LMTP endpoint on Unix socket delivering to IMAP storage
# in previous config snippet.
lmtp unix:/run/maddy/lmtp.sock {
    hostname mx.maddy.test

    deliver_to &local_mailboxes
}

Look up documentation for your SMTP server on how to make it
send messages using LMTP to /run/maddy/lmtp.sock.

To handle authentication for Submission (client-server SMTP) SMTP server
needs to access credentials database used by maddy. maddy implements
server side of Dovecot authentication protocol so you can use
it if SMTP server implements "Dovecot SASL" client.

To create a Dovecot-compatible sasld endpoint, add the following configuration
block:

text
# Dovecot-compatible sasld endpoint using data from local_authdb.
dovecot_sasld unix:/run/maddy/auth-client.sock {
    auth &local_authdb
}

Tutorials Alias To Remote (`docs/tutorials/alias-to-remote.md`)

Forward messages to a remote MX

Default maddy configuration is done in a way that does not result in any
outbound messages being sent as a result of port 25 traffic.

In particular, this means that if you handle messages for example.org but not
example.com and have the following in your aliases file (e.g. /etc/maddy/aliases):

You will get "User does not exist" error when attempting to send a message to
[email protected] because [email protected] does not exist on as a local
user.

Some users may want to make it work, but it is important to understand the
consequences of such configuration:

  • Flooding your server will also flood the remote server.
  • If your spam filtering is not good enough, you will send spam to the remote
    server.

In both cases, you might harm the reputation of your server (e.g. get your IP
listed in a DNSBL).

So, this is a bad practice. Do so only if you clearly understand the
consequences (including the Bounce handling section below).

If you want to do it anyway, here is the part of the configuration that needs
tweaking:

text
msgpipeline local_routing {
    destination postmaster $(local_domains) {
        modify {
            replace_rcpt regexp "(.+)\+(.+)@(.+)" "$1@$3"
            replace_rcpt file /etc/maddy/aliases
        }

        deliver_to &local_mailboxes
    }

    default_destination {
        reject 550 5.1.1 "User doesn't exist"
    }
}

In default configuration, local_routing block is responsible for handling
messages that are received via SMTP or Submission and have the initial
destination address at a local domain.

Note the modify { } block being nested inside destination and then followed
by unconditional deliver_to &local_mailboxes. This means: if address is
on $(local_domains), apply aliases and deliver to mailboxes from
&local_mailboxes.

The problem here is that recipients are matched before aliases are resolved so
in the end, maddy attempts to look up [email protected] locally. The solution
is to insert another step into the pipeline configuration to rerun matching
after aliases are resolved. This can be done using the 'reroute' directive:

text
msgpipeline local_routing {
    destination postmaster $(local_domains) {
        modify {
            replace_rcpt file /etc/maddy/aliases
			...
        }

		reroute {
			destination postmaster $(local_domains) {
				deliver_to &local_mailboxes
			}
			default_destination {
				deliver_to &remote_queue
			}
		}
    }

    default_destination {
        reject 550 5.1.1 "User doesn't exist"
    }
}

Bounce handling

Once the message is delivered to remote_queue, it will follow the usual path
for outbound delivery, including queuing and multiple attempts. This also
means bounce messages will be generated on failures. When accepting messages
from arbitrary senders via the 25 port, the DSN recipient will be whatever
sender specifies in the MAIL FROM command. This is prone to collateral spam
when an automatically generated bounce message gets sent to a spoofed address.

However, the default maddy configuration ensures that in this case, the NDN
will be delivered only if the original sender is a local user. Backscatter can
not happen if the sender spoofed a local address since such messages will not
be accepted in the first place.

You can also configure maddy to send bounce messages to remote
addresses, but in this case, you should configure a really strict local policy
to make sure the sender address is not spoofed. There is no detailed
explanation of how to do this since this is a terrible idea in general.

Transparent forwarding

As an alternative to silently dropping messages on remote delivery failures,
you might want to use transparent forwarding and reject the message without
accepting it first ("connection-stage rejection").

To do so, simply do not use the queue, replace

text
deliver_to &remote_queue

with

text
deliver_to &outbound_delivery

(assuming outbound_delivery refers to target.remote block)


Tutorials Building From Source (`docs/tutorials/building-from-source.md`)

Building from source

System dependencies

You need C toolchain, Go toolchain and Make:

On Debian-based system this should work:

text
apt-get install golang-1.26 gcc libc6-dev make

Additionally, if you want manual pages, you should also have scdoc installed.
Figuring out the appropriate way to get scdoc is left as an exercise for
reader (for Ubuntu 22.04 LTS it is in repositories).

Recent Go toolchain

maddy depends on a rather recent Go toolchain version that may not be
available in some distributions (cough Debian cough).

go command in Go 1.21 or newer will automatically download up-to-date
toolchain to build maddy. It is necessary to run commands below only
if you have go command version older than 1.21.

text
wget "https://go.dev/dl/go1.26.1.linux-amd64.tar.gz"
tar xf "go1.26.1.linux-amd64.tar.gz"
export GOROOT="$PWD/go"
export PATH="$PWD/go/bin:$PATH"

Step-by-step

  1. Clone repository
text
$ git clone https://github.com/foxcpp/maddy.git
$ cd maddy
  1. Select the appropriate version to build:
text
$ git checkout v0.8.0      # a specific release
$ git checkout master      # next bugfix release
$ git checkout dev         # next feature release
  1. Build & install it
text
$ ./build.sh
$ sudo ./build.sh install
  1. Finish setup as described in Setting up (starting from System configuration).

Tutorials Pam (`docs/tutorials/pam.md`)

Using PAM authentication

maddy supports user authentication using PAM infrastructure via auth.pam
module.

In order to use it, however, either maddy itself should be compiled
with libpam support or a helper executable should be built and
installed into an appropriate directory.

It is recommended to use builtin libpam support if you are using
PAM as an intermediate for authentication provider not directly
supported by maddy.

If PAM authentication requires privileged access on the host system
(e.g. pam_unix.so aka /etc/shadow) then it is recommended to use
a privileged helper executable since maddy process itself won't
have access to it.

Built-in PAM support

Binary artifacts provided for releases do not come with
libpam support. You should build maddy from source.

See here for detailed instructions.

You should have libpam development files installed (libpam-dev
package on Ubuntu/Debian).

Then add --tags 'libpam' to the build command:

text
./build.sh --tags 'libpam'

Then you should be able to replace local_authdb implementation
in default configuration with auth.pam:

text
auth.pam local_authdb {
    use_helper no
}

Helper executable

TL;DR

text
git clone https://github.com/foxcpp/maddy
cd maddy/cmd/maddy-pam-helper
gcc pam.c main.c -lpam -o maddy-pam-helper

Copy the resulting executable into /usr/lib/maddy/ and make
it setuid-root so it can read /etc/shadow (if that's necessary):

text
chown root:maddy /usr/lib/maddy/maddy-pam-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helper

Then you should be able to replace local_authdb implementation
in default configuration with auth.pam:

text
auth.pam local_authdb {
    use_helper yes
}

Account names

Since PAM does not use emails for authentication you should configure
maddy to either strip domain part when checking credentials or do not
use email when authenticating.

See Multiple domains configuration for how to configure
authentication.

PAM service

You should create a PAM configuration file for maddy to use.
Place it into /etc/pam.d/maddy.
Here is the minimal example using pam_unix (shadow database).

text
#%PAM-1.0
auth	required	pam_unix.so
account	required	pam_unix.so

Here is the configuration example you could use on Ubuntu
to use the authentication config system itself uses:

text
#%PAM-1.0

@include common-auth
@include common-account
@include common-session

Tutorials Setting Up (`docs/tutorials/setting-up.md`)

Installation & initial configuration

This is the practical guide on how to set up a mail server using maddy for
personal use. It omits most of the technical details for brevity and just gives
you the minimal list of things you need to be aware of and what to do to make
stuff work.

For purposes of clarity, these values are used in this tutorial as examples,
wherever you see them, you need to replace them with your actual values:

  • Domain: example.org
  • MX domain (hostname): mx1.example.org
  • IPv4 address: 10.2.3.4
  • IPv6 address: 2001:beef::1

Getting a server

Where to get a server to run maddy on is out of the scope of this article. Any
VPS (virtual private server) will work fine for small configurations. However,
there are a few things to keep in mind:

  • Make sure your provider does not block SMTP traffic (25 TCP port). Most VPS
    providers don't do it, but some "cloud" providers (such as Google Cloud) do
    it, so you can't host your mail there.

  • It is recommended to run your own DNS resolver with DNSSEC verification
    enabled.

Installing maddy

Your options are:

  • Pre-built tarball (Linux, amd64)

    Available on GitHub or
    maddy.email/builds.

    The tarball includes maddy executable you can
    copy into /usr/local/bin as well as systemd unit file you can
    use on systemd-based distributions for automatic startup and service
    supervision. You should also create "maddy" user and group.
    See below for more detailed instructions.

  • Docker image (Linux, amd64)

    text
    docker pull foxcpp/maddy:0.6

    See here for Docker-specific instructions.

  • Building from source

    See here for instructions.

  • Arch Linux packages

    For Arch Linux users, maddy and maddy-git PKGBUILDs are available
    in AUR. Additionally, binary packages are available in 3rd-party
    repository at https://maddy.email/archlinux/

System configuration (systemd-based distribution)

If you built maddy from source and used ./build.sh install then
systemd unit files should be already installed. If you used
a pre-built tarball - copy systemd/*.service to /etc/systemd/system
manually.

You need to reload service manager configuration to make service available:

text
systemctl daemon-reload

Additionally, you should create maddy user and group. Unlike most other
Linux mail servers, maddy never runs as root.

text
useradd -mrU -s /sbin/nologin -d /var/lib/maddy -c "maddy mail server" maddy

Host name + domain

Open /etc/maddy/maddy.conf with vim^W your favorite editor and change
the following lines to match your server name and domain you want to handle
mail for.
If you setup a very small mail server you can use example.org in both fields.
However, to easier a future migration of service, it's recommended to use a
separate DNS entry for that purpose. It's usually mx1.example.org, mx2, etc.
You can of course use another subdomain, for instance: smtp1.example.org.
An email failover server will become possible if you forward mx2.example.org
to another server (as long as you configure it to handle your domain).

text
$(hostname) = mx1.example.org
$(primary_domain) = example.org

If you want to handle multiple domains, you still need to designate
one as "primary". Add all other domains to the local_domains line:

text
$(local_domains) = $(primary_domain) example.com other.example.com

Do not forget to set a suitable rDNS (PTR) record for your server's IP address
to reduce the chances of outgoing mails getting marked as spam or being
downright rejected. Ideally, the PTR record should match whatever you specified
in $(hostname).

TLS certificates

One thing that can't be automatically configured is TLS certs. If you already
have them somewhere - use them, open /etc/maddy/maddy.conf and put the right
paths in. You need to make sure maddy can read them while running as
unprivileged user (maddy never runs as root, even during start-up), one way to
do so is to use ACLs (replace with your actual paths):

text
$ sudo setfacl -R -m u:maddy:rX /etc/ssl/mx1.example.org.crt /etc/ssl/mx1.example.org.key

maddy reloads TLS certificates from disk once in a minute so it will notice
renewal. It is possible to force reload via systemctl reload maddy (or just
killall -USR2 maddy).

Let's Encrypt and certbot

If you use certbot to manage your certificates, you can simply symlink
/etc/maddy/certs into /etc/letsencrypt/live. maddy will pick the right
certificate depending on the domain you specified during installation.

You still need to make keys readable for maddy, though:

text
$ sudo setfacl -R -m u:maddy:rX /etc/letsencrypt/{live,archive}

ACME.sh

If you use acme.sh to manage your certificates, you could simply run:

text
mkdir -p /etc/maddy/certs/mx1.example.org
acme.sh --force --install-cert -d mx1.example.org \
  --key-file       /etc/maddy/certs/mx1.example.org/privkey.pem  \
  --fullchain-file /etc/maddy/certs/mx1.example.org/fullchain.pem

First run

text
systemctl start maddy

The daemon should be running now, except that it is useless because we haven't
configured DNS records.

DNS records

How it is configured depends on your DNS provider (or server, if you run your
own). Here is how your DNS zone should look like:

text
; Basic domain->IP records, you probably already have them.
example.org.   A     10.2.3.4
example.org.   AAAA  2001:beef::1

; It says that "server mx1.example.org is handling messages for example.org".
example.org.   MX    10 mx1.example.org.
; Of course, mx1 should have A/AAAA entry as well:
mx1.example.org.   A     10.2.3.4
mx1.example.org.   AAAA  2001:beef::1

; Use SPF to say that the servers in "MX" above are allowed to send email
; for this domain, and nobody else.
example.org.     TXT   "v=spf1 mx ~all"
; It is recommended to server SPF record for both domain and MX hostname
mx1.example.org. TXT   "v=spf1 a ~all"

; Opt-in into DMARC with permissive policy and request reports about broken
; messages.
_dmarc.example.org.   TXT    "v=DMARC1; p=quarantine; ruf=mailto:[email protected]"

; Mark domain as MTA-STS compatible (see the next section)
; and request reports about failures to be sent to [email protected]
_mta-sts.example.org.   TXT    "v=STSv1; id=1"
_smtp._tls.example.org. TXT    "v=TLSRPTv1;rua=mailto:[email protected]"

And the last one, DKIM key, is a bit tricky. maddy generated a key for you on
the first start-up. You can find it in
/var/lib/maddy/dkim_keys/example.org_default.dns. You need to put it in a TXT
record for default._domainkey.example.org. domain, like that:

text
default._domainkey.example.org.    TXT   "v=DKIM1; k=ed25519; p=nAcUUozPlhc4VPhp7hZl+owES7j7OlEv0laaDEDBAqg="

MTA-STS and DANE

By default SMTP is not protected against active attacks. MTA-STS policy tells
compatible senders to always use properly authenticated TLS when talking to
your server, offering a simple-to-deploy way to protect your server against
MitM attacks on port 25.

Basically, you to create a file with following contents and make it available
at https://mta-sts.example.org/.well-known/mta-sts.txt:

text
version: STSv1
mode: enforce
max_age: 604800
mx: mx1.example.org

Note: mx1.example.org in the file is your MX hostname, In a simple configuration,
it will be the same as your hostname example.org.
In a more complex setups, you would have multiple MX servers - add them all once
per line, like that:

text
mx: mx1.example.org
mx: mx2.example.org

It is also recommended to set a TLSA (DANE) record.
Use https://www.huque.com/bin/gen_tlsa to generate one.
Set port to 25, Transport Protocol to "tcp" and Domain Name to the MX hostname.
Example of a valid record:

text
_25._tcp.mx1.example.org. TLSA 3 1 1 7f59d873a70e224b184c95a4eb54caa9621e47d48b4a25d312d83d96e3498238

User accounts and maddy command

A mail server is useless without mailboxes, right? Unlike software like postfix
and dovecot, maddy uses "virtual users" by default, meaning it does not care or
know about system users.

IMAP mailboxes ("accounts") and authentication credentials are kept separate.

To register user credentials, use maddy creds create command.
Like that:

text
$ maddy creds create [email protected]

Note the username is a e-mail address. This is required as username is used to
authorize IMAP and SMTP access (unless you configure custom mappings, not
described here).

After registering the user credentials, you also need to create a local
storage account:

text
$ maddy imap-acct create [email protected]

Note: to run maddy CLI commands, your user should be in the maddy
group. Alternatively, just use sudo -u maddy.

That is it. Now you have your first e-mail address. when authenticating using
your e-mail client, do not forget the username is "[email protected]", not
just "postmaster".

You may find running maddy creds --help and maddy imap-acct --help
useful to learn about other commands. Note that IMAP accounts and credentials
are managed separately yet usernames should match by default for things to
work.


Reference Auth Dovecot Sasl (`docs/reference/auth/dovecot_sasl.md`)

Dovecot SASL

The 'auth.dovecot_sasl' module implements the client side of the Dovecot
authentication protocol, allowing maddy to use it as a credentials source.

Currently SASL mechanisms support is limited to mechanisms supported by maddy
so you cannot get e.g. SCRAM-MD5 this way.

text
auth.dovecot_sasl {
	endpoint unix://socket_path
}

dovecot_sasl unix://socket_path

Configuration directives

endpoint _schema://address_

Default: not set

Set the address to use to contact Dovecot SASL server in the standard endpoint
format.

tcp://10.0.0.1:2222 for TCP, unix:///var/lib/dovecot/auth.sock for Unix
domain sockets.


Reference Auth External (`docs/reference/auth/external.md`)

System command

auth.external module for authentication using external helper binary. It looks for binary
named maddy-auth-helper in $PATH and libexecdir and uses it for authentication
using username/password pair.

The protocol is very simple:
Program is launched for each authentication. Username and password are written
to stdin, adding \n to the end. If binary exits with 0 status code -
authentication is considered successful. If the status code is 1 -
authentication is failed. If the status code is 2 - another unrelated error has
happened. Additional information should be written to stderr.

text
auth.external {
    helper /usr/bin/ldap-helper
    perdomain no
    domains example.org
}

Configuration directives

helper _file_path_

Required.
Location of the helper binary.


perdomain _boolean_

Default: no

Don't remove domain part of username when authenticating and require it to be
present. Can be used if you want user@domain1 and user@domain2 to be different
accounts.


domains _domains..._

Default: not specified

Domains that should be allowed in username during authentication.

For example, if 'domains' is set to "domain1 domain2", then
username, username@domain1 and username@domain2 will be accepted as valid login
name in addition to just username.

If used without 'perdomain', domain part will be removed from login before
check with underlying auth. mechanism. If 'perdomain' is set, then
domains must be also set and domain part will not be removed before check.


Reference Auth Ldap (`docs/reference/auth/ldap.md`)

LDAP BindDN

maddy supports authentication via LDAP using DN binding. Passwords are verified
by the LDAP server.

maddy needs to know the DN to use for binding. It can be obtained either by
directory search or template .

Note that storage backends conventionally use email addresses, if you use
non-email identifiers as usernames then you should map them onto
emails on delivery by using auth_map (see documentation page for used storage backend).

auth.ldap also can be a used as a table module. This way you can check
whether the account exists. It works only if DN template is not used.

text
auth.ldap {
    urls ldap://maddy.test:389

    # Specify initial bind credentials. Not required ('bind off')
    # if DN template is used.
    bind plain "cn=maddy,ou=people,dc=maddy,dc=test" "123456"

    # Specify DN template to skip lookup.
    dn_template "cn={username},ou=people,dc=maddy,dc=test"

    # Specify base_dn and filter to lookup DN.
    base_dn "ou=people,dc=maddy,dc=test"
    filter "(&(objectClass=posixAccount)(uid={username}))"

    tls_client { ... }
    starttls off
    debug off
    connect_timeout 1m
}
text
auth.ldap ldap://maddy.test.389 {
    ...
}

Configuration directives

urls _servers..._

Required.

URLs of the directory servers to use. First available server
is used - no load-balancing is done.

URLs should use ldap://, ldaps://, ldapi:// schemes.


bind `off` | `unauth` | `external` | `plain` _username_ _password_

Default: off

Credentials to use for initial binding. Required if DN lookup is used.

unauth performs unauthenticated bind. external performs external binding
which is useful for Unix socket connections (ldapi://) or TLS client certificate
authentication (cert. is set using tls_client directive). plain performs a
simple bind using provided credentials.


dn_template _template_

DN template to use for binding. {username} is replaced with the
username specified by the user.


base_dn _dn_

Base DN to use for lookup.


filter _str_

DN lookup filter. {username} is replaced with the username specified
by the user.

Example:

text
(&(objectClass=posixAccount)(uid={username}))

Example (using ActiveDirectory):

text
(&(objectCategory=Person)(memberOf=CN=user-group,OU=example,DC=example,DC=org)(sAMAccountName={username})(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))

Example:

text
(&(objectClass=Person)(mail={username}))

starttls _bool_

Default: off

Whether to upgrade connection to TLS using STARTTLS.


tls_client { ... }

Advanced TLS client configuration. See TLS configuration / Client for details.


connect_timeout _duration_

Default: 1m

Timeout for initial connection to the directory server.


request_timeout _duration_

Default: 1m

Timeout for each request (binding, lookup).


Reference Auth Netauth (`docs/reference/auth/netauth.md`)

Native NetAuth

maddy supports authentication via NetAuth using direct entity
authentication checks. Passwords are verified by the NetAuth server.

maddy needs to know the Entity ID to use for authentication. It must
match the string the user provides for the Local Atom part of their
mail address.

Note that storage backends conventionally use email addresses. Since NetAuth
recommends *nix compatible usernames. You will need to either map email
identifiers specified by user to NetAuth Entity IDs using auth_map in
endpoint.smtp/imap configuration (recommended) or you would need to use
storage_map in storage backend configuration to map NetAuth Entity ID
specified by user back to appropriate storage backend account names.

auth.netauth also can be used as a table module. This way you can
check whether the account exists.

Note that the configuration fragment provided below is very sparse.
This is because NetAuth expects to read most of its common
configuration values from the system NetAuth config file located at
/etc/netauth/config.toml.

text
auth.netauth {
  require_group "maddy-users"
  debug off
}
text
auth.netauth {}

Configuration directives

require_group _group_

Optional.

Group that entities must possess to be able to use maddy services.
This can be used to provide email to just a subset of the entities
present in NetAuth.


debug `on` | `off`

Default: off


Reference Auth Pam (`docs/reference/auth/pam.md`)

PAM

auth.pam module implements authentication using libpam. Alternatively it can be configured to
use helper binary like auth.external module does.

maddy should be built with libpam build tag to use this module without
'use_helper' directive.

text
go get -tags 'libpam' ...
text
auth.pam {
    debug no
    use_helper no
}

Configuration directives

debug _boolean_

Default: no

Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.


use_helper _boolean_

Default: no

Use LibexecDirectory/maddy-pam-helper instead of directly calling libpam.
You need to use that if:

  1. maddy is not compiled with libpam, but maddy-pam-helper is built separately.
  2. maddy is running as an unprivileged user and used PAM configuration requires additional privileges (e.g. when using system accounts).

For 2, you need to make maddy-pam-helper binary setuid, see
TL;DR (assuming you have the maddy group):

text
chown root:maddy /usr/lib/maddy/maddy-pam-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helper

Reference Auth Pass Table (`docs/reference/auth/pass_table.md`)

Password table

auth.pass_table module implements username:password authentication by looking up the
password hash using a table module (maddy-tables(5)). It can be used
to load user credentials from text file (via table.file module) or SQL query
(via table.sql_table module).

Definition:

text
auth.pass_table [block name] {
	table <table config>

}

Shortened variant for inline use:

text
pass_table <table> [table arguments] {
	[additional table config]
}

Example, read username:password pair from the text file:

text
smtp tcp://0.0.0.0:587 {
	auth pass_table file /etc/maddy/smtp_passwd
	...
}

Password hashes

pass_table expects the used table to contain certain structured values with
hash algorithm name, salt and other necessary parameters.

You should use maddy hash command to generate suitable values.
See maddy hash --help for details.

maddy creds

If the underlying table is a "mutable" table (see maddy-tables(5)) then
the maddy creds command can be used to modify the underlying tables
via pass_table module. It will act on a "local credentials store" and will write
appropriate hash values to the table.


Reference Auth Plain Separate (`docs/reference/auth/plain_separate.md`)

Separate username and password lookup

auth.plain_separate module implements authentication using username:password pairs but can
use zero or more "table modules" (maddy-tables(5)) and one or more
authentication providers to verify credentials.

text
auth.plain_separate {
	user ...
	user ...
	...
	pass ...
	pass ...
	...
}

How it works:

  • Initial username input is normalized using PRECIS UsernameCaseMapped profile.
  • Each table specified with the 'user' directive looked up using normalized
    username. If match is not found in any table, authentication fails.
  • Each authentication provider specified with the 'pass' directive is tried.
    If authentication with all providers fails - an error is returned.

Configuration directives

user _table-module_

Configuration block for any module from maddy-tables(5) can be used here.

Example:

text
user file /etc/maddy/allowed_users

pass _auth-provider_

Configuration block for any auth. provider module can be used here, even
'plain_separate' itself.

The used auth. provider must provide username:password pair-based
authentication.


no_pass _boolean_

Allow not specifying 'pass' directive. This disables password checks altogether
and is generally unsafe.


Reference Auth Shadow (`docs/reference/auth/shadow.md`)

/etc/shadow

auth.shadow module implements authentication by reading /etc/shadow. Alternatively it can be
configured to use helper binary like auth.external does.

text
auth.shadow {
    debug no
    use_helper no
}

Configuration directives

debug _boolean_

Default: no

Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.


use_helper _boolean_

Default: no

Use LibexecDirectory/maddy-shadow-helper instead of directly reading /etc/shadow.
You need to use that if maddy is running as an unprivileged user
privileges (e.g. when using system accounts).

You need to make maddy-shadow-helper binary setuid, see
cmd/maddy-shadow-helper/README.md in source tree for details.

TL;DR (assuming you have maddy group):

text
chown root:maddy /usr/lib/maddy/maddy-shadow-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-shadow-helper

Reference Blob Fs (`docs/reference/blob/fs.md`)

Filesystem

This module stores message bodies in a file system directory.

text
storage.blob.fs {
    root <directory>
}
text
storage.blob.fs <directory>

Configuration directives

root _path_

Default: not set

Path to the FS directory. Must be readable and writable by the server process.
If it does not exist - it will be created (parent directory should be writable
for this). Relative paths are interpreted relatively to server state directory.


Reference Blob S3 (`docs/reference/blob/s3.md`)

Amazon S3

storage.blob.s3 module stores messages bodies in a bucket on S3-compatible storage.

text
storage.blob.s3 {
    endpoint play.min.io
    secure yes
    access_key "Q3AM3UQ867SPQQA43P2F"
    secret_key "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
    bucket maddy-test

    # optional
    region eu-central-1
    object_prefix maddy/
    creds access_key
}

Example:

text
storage.imapsql local_mailboxes {
    ...
    msg_store s3 {
        endpoint s3.amazonaws.com
        access_key "..."
        secret_key "..."
        bucket maddy-messages
        region us-west-2
        creds access_key
    }
}

Configuration directives

endpoint _address:port_

Required.

Root S3 endpoint. e.g. s3.amazonaws.com


secure _boolean_

Default: yes

Whether TLS should be used.


access_key _string_secret_key _string_

Required.

Static S3 credentials.


bucket _name_

Required.

S3 bucket name. The bucket must exist and
be read-writable.


region _string_

Default: not set

S3 bucket location. May be called "endpoint" in some manuals.


object_prefix _string_

Default: empty string

String to add to all keys stored by maddy.

Can be useful when S3 is used as a file system.


creds `access_key` | `file_minio` | `file_aws` | `iam`

Default: access_key

Credentials to use for accessing the S3 Bucket.

Credential Types:

  • access_key: use AWS access key and secret access key
  • file_minio: use credentials for Minio present at ~/.mc/config.json
  • file_aws: use credentials for AWS S3 present at ~/.aws/credentials
  • iam: use AWS IAM instance profile for credentials.

By default, access_key is used with the access key and secret access key present in the config.


Reference Checks Actions (`docs/reference/checks/actions.md`)

Check actions

When a certain check module thinks the message is "bad", it takes some actions
depending on its configuration. Most checks follow the same configuration
structure and allow following actions to be taken on check failure:

  • Do nothing (action ignore)

Useful for testing deployment of new checks. Check failures are still logged
but they have no effect on message delivery.

  • Reject the message (action reject)

Reject the message at connection time. No bounce is generated locally.

  • Quarantine the message (action quarantine)

Mark message as 'quarantined'. If message is then delivered to the local
storage, the storage backend can place the message in the 'Junk' mailbox.
Another thing to keep in mind that 'target.remote' module
will refuse to send quarantined messages.


Reference Checks Authorize Sender (`docs/reference/checks/authorize_sender.md`)

MAIL FROM and From authorization

Module check.authorize_sender verifies that envelope and header sender addresses belong
to the authenticated user. Address ownership is established via table
that maps each user account to a email address it is allowed to use.
There are some special cases, see user_to_email description below.

text
check.authorize_sender {
    prepare_email identity
    user_to_email identity
    check_header yes

    unauth_action reject
    no_match_action reject
    malformed_action reject
    err_action reject

    auth_normalize auto
    from_normalize auto
}
text
check {
    authorize_sender { ... }
}

Configuration directives

user_to_email _table_

Default: identity

Table that maps authorization username to the list of sender emails
the user is allowed to use.

In additional to email addresses, the table can contain domain names or
special string "*" as a value. If the value is a domain - user
will be allowed to use any mailbox within it as a sender address.
If it is "*" - user will be allowed to use any address.

By default, table.identity is used, meaning that username should
be equal to the sender email.

Before username is looked up via the table, normalization algorithm
defined by auth_normalize is applied to it.


prepare_email _table_

Default: identity

Table that is used to translate email addresses before they
are matched against user_to_email values.

Typically used to allow users to use their aliases as sender
addresses - prepare_email in this case should translate
aliases to "canonical" addresses. This is how it is
done in default configuration.

If table does not contain any mapping for the used sender
address, it will be used as is.


check_header _boolean_

Default: yes

Whether to verify header sender in addition to envelope.

Either Sender or From field value should match the
authorization identity.


unauth_action _action_

Default: reject

What to do if the user is not authenticated at all.


no_match_action _action_

Default: reject

What to do if user is not allowed to use the sender address specified.


malformed_action _action_

Default: reject

What to do if From or Sender header fields contain malformed values.


err_action _action_

Default: reject

What to do if error happens during prepare_email or user_to_email lookup.


auth_normalize _action_

Default: auto

Normalization function to apply to authorization username before
further processing.

Available options:

  • auto precis_casefold_email for valid emails, precis_casefold otherwise.
  • precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain
  • precis_casefold PRECIS UsernameCaseMapped profile for the entire string
  • precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain
  • precis PRECIS UsernameCasePreserved profile for the entire string
  • casefold Convert to lower case
  • noop Nothing

PRECIS profiles are defined by RFC 8265. In short, they make sure
that Unicode strings that look the same will be compared as if they were
the same. CaseMapped profiles also convert strings to lower case.


from_normalize _action_

Default: auto

Normalization function to apply to email addresses before
further processing.

Available options are same as for auth_normalize.


Reference Checks Command (`docs/reference/checks/command.md`)

System command filter

This module executes an arbitrary system command during a specified stage of
checks execution.

text
command executable_name arg0 arg1 ... {
	run_on body

	code 1 reject
	code 2 quarantine
}

Arguments

The module arguments specify the command to run. If the first argument is not
an absolute path, it is looked up in the Libexec Directory (/usr/lib/maddy on
Linux) and in $PATH (in that ordering). Note that no additional handling
of arguments is done, especially, the command is executed directly, not via the
system shell.

There is a set of special strings that are replaced with the corresponding
message-specific values:

  • {source_ip} – IPv4/IPv6 address of the sending MTA.
  • {source_host} – Hostname of the sending MTA, from the HELO/EHLO command.
  • {source_rdns} – PTR record of the sending MTA IP address.
  • {msg_id} – Internal message identifier. Unique for each delivery.
  • {auth_user} – Client username, if authenticated using SASL PLAIN
  • {sender} – Message sender address, as specified in the MAIL FROM SMTP command.
  • {rcpts} – List of accepted recipient addresses, including the currently handled
    one.
  • {address} – Currently handled address. This is a recipient address if the command
    is called during RCPT TO command handling (run_on rcpt) or a sender
    address if the command is called during MAIL FROM command handling (run_on sender).

If value is undefined (e.g. {source_ip} for a message accepted over a Unix
socket) or unavailable (the command is executed too early), the placeholder
is replaced with an empty string. Note that it can not remove the argument.
E.g. -i {source_ip} will not become just -i, it will be -i ""

Undefined placeholders are not replaced.

Command stdout

The command stdout must be either empty or contain a valid RFC 5322 header.
If it contains a byte stream that does not look a valid header, the message
will be rejected with a temporary error.

The header from stdout will be prepended to the message header.

Configuration directives

run_on `conn` | `sender` | `rcpt` | `body`

Default: body

When to run the command. This directive also affects the information visible
for the message.

  • conn
    Run before the sender address (MAIL FROM) is handled.
    Stdin: Empty
    Available placeholders: {source_ip}, {source_host}, {msg_id}, {auth_user}.

  • sender
    Run during sender address (MAIL FROM) handling.
    Stdin: Empty
    Available placeholders: conn placeholders + {sender}, {address}.
    The {address} placeholder contains the MAIL FROM address.

  • rcpt
    Run during recipient address (RCPT TO) handling. The command is executed
    once for each RCPT TO command, even if the same recipient is specified
    multiple times.
    Stdin: Empty
    Available placeholders: sender placeholders + {rcpts}.
    The {address} placeholder contains the recipient address.

  • body
    Run during message body handling.
    Stdin: The message header + body
    Available placeholders: all except for {address}.


code _integer_ ignore code _integer_ quarantine code _integer_ reject _smtp-code_ _smtp-enhanced-code_ _smtp-message_

This directive specifies the mapping from the command exit code integer to
the message pipeline action.

Two codes are defined implicitly, exit code 1 causes the message to be rejected
with a permanent error, exit code 2 causes the message to be quarantined. Both
actions can be overridden using the 'code' directive.


Reference Checks Dkim (`docs/reference/checks/dkim.md`)

DKIM

This is the check module that performs verification of the DKIM signatures
present on the incoming messages.

Configuration directives

text
check.dkim {
    debug no
    required_fields From Subject
    allow_body_subset no
    no_sig_action ignore
    broken_sig_action ignore
	fail_open no
}

debug _boolean_

Default: global directive value

Log both successful and unsuccessful check executions instead of just
unsuccessful.


required_fields _string..._

Default: From Subject

Header fields that should be included in each signature. If signature
lacks any field listed in that directive, it will be considered invalid.

Note that From is always required to be signed, even if it is not included in
this directive.


no_sig_action _action_

Default: ignore (recommended by RFC 6376)

Action to take when message without any signature is received.

Note that DMARC policy of the sender domain can request more strict handling of
missing DKIM signatures.


broken_sig_action _action_

Default: ignore (recommended by RFC 6376)

Action to take when there are not valid signatures in a message.

Note that DMARC policy of the sender domain can request more strict handling of
broken DKIM signatures.


fail_open _boolean_

Default: no

Whether to accept the message if a temporary error occurs during DKIM
verification. Rejecting the message with a 4xx code will require the sender
to resend it later in a hope that the problem will be resolved.


Reference Checks Dnsbl (`docs/reference/checks/dnsbl.md`)

DNSBL lookup

The check.dnsbl module implements checking of source IP and hostnames against a set
of DNS-based Blackhole lists (DNSBLs).

Its configuration consists of module configuration directives and a set
of blocks specifying lists to use and kind of lookups to perform on them.

text
check.dnsbl {
    debug no
    check_early no

    quarantine_threshold 1
    reject_threshold 1

    # Lists configuration example.
    dnsbl.example.org {
        client_ipv4 yes
        client_ipv6 no
        ehlo no
        mailfrom no
        score 1
    }
    hsrbl.example.org {
        client_ipv4 no
        client_ipv6 no
        ehlo yes
        mailfrom yes
        score 1
    }
    
    # Example with per-response-code scoring (new in 0.8)
    zen.spamhaus.org {
        client_ipv4 yes
        client_ipv6 yes
        
        # SBL - Spamhaus Block List (known spam sources)
        response 127.0.0.2 127.0.0.3 {
            score 10
            message "Listed in Spamhaus SBL. See https://check.spamhaus.org/"
        }
        
        # XBL - Exploits Block List (compromised hosts)
        response 127.0.0.4 127.0.0.5 127.0.0.6 127.0.0.7 {
            score 10
            message "Listed in Spamhaus XBL. See https://check.spamhaus.org/"
        }
        
        # PBL - Policy Block List (dynamic IPs)
        response 127.0.0.10 127.0.0.11 {
            score 5
            message "Listed in Spamhaus PBL. See https://check.spamhaus.org/"
        }
    }
}

Arguments

Arguments specify the list of IP-based BLs to use.

The following configurations are equivalent.

text
check {
    dnsbl dnsbl.example.org dnsbl2.example.org
}
text
check {
    dnsbl {
        dnsbl.example.org dnsbl2.example.org {
            client_ipv4 yes
            client_ipv6 no
            ehlo no
            mailfrom no
            score 1
        }
    }
}

Configuration directives

debug _boolean_

Default: global directive value

Enable verbose logging.


check_early _boolean_

Default: no

Check BLs before mail delivery starts and silently reject blacklisted clients.

For this to work correctly, check should not be used in source/destination
pipeline block.

In particular, this means:

  • No logging is done for rejected messages.
  • No action is taken if quarantine_threshold is hit, only reject_threshold
    applies.
  • defer_sender_reject from SMTP configuration takes no effect.
  • MAIL FROM is not checked, even if specified.

If you often get hit by spam attacks, it is recommended to enable this
setting to save server resources.


quarantine_threshold _integer_

Default: 1

DNSBL score needed (equals-or-higher) to quarantine the message.


reject_threshold _integer_

Default: 9999

DNSBL score needed (equals-or-higher) to reject the message.

List configuration

text
dnsbl.example.org dnsbl.example.com {
    client_ipv4 yes
    client_ipv6 no
    ehlo no
    mailfrom no
    responses 127.0.0.1/24
	score 1
}

Directive name and arguments specify the actual DNS zone to query when checking
the list. Using multiple arguments is equivalent to specifying the same
configuration separately for each list.

client_ipv4 _boolean_

Default: yes

Whether to check address of the IPv4 clients against the list.


client_ipv6 _boolean_

Default: yes

Whether to check address of the IPv6 clients against the list.


ehlo _boolean_

Default: no

Whether to check hostname specified n the HELO/EHLO command
against the list.

This works correctly only with domain-based DNSBLs.


mailfrom _boolean_

Default: no

Whether to check domain part of the MAIL FROM address against the list.

This works correctly only with domain-based DNSBLs.


responses _cidr_ | _ip..._

Default: 127.0.0.1/24

IP networks (in CIDR notation) or addresses to permit in list lookup results.
Addresses not matching any entry in this directives will be ignored.


score _integer_

Default: 1

Score value to add for the message if it is listed.

If sum of list scores is equals or higher than quarantine_threshold, the
message will be quarantined.

If sum of list scores is equals or higher than rejected_threshold, the message
will be rejected.

It is possible to specify a negative value to make list act like a whitelist
and override results of other blocklists.

Note: When using response blocks (see below), the score from matching response
rules is used instead of this flat score value.


response _ip..._

Defines per-response-code rules for scoring and custom messages. This is useful
for combined DNSBLs like Spamhaus ZEN that return different codes for different
listing types.

This works for both IP-based lookups (client_ipv4, client_ipv6) and domain-based
lookups (ehlo, mailfrom).

Each response block takes one or more IP addresses or CIDR ranges as arguments
and contains the following directives:

score _integer_

Required

Score to add when this response code is returned. If multiple response codes
are returned by the DNSBL, and they match different rules, the scores from
all matched rules are summed together. Each rule is counted only once, even
if multiple returned IPs match networks within that rule.

message _string_

Optional

Custom rejection or quarantine message to include when this response code
matches. This message is shown to the client or logged when the threshold
is reached.

Example:

text
zen.spamhaus.org {
    client_ipv4 yes
    
    # High severity - known spam sources
    response 127.0.0.2 127.0.0.3 {
        score 10
        message "Listed in Spamhaus SBL"
    }
    
    # Lower severity - dynamic IPs
    response 127.0.0.10 127.0.0.11 {
        score 5
        message "Listed in Spamhaus PBL"
    }
}

Scoring behavior:

  • If DNSBL returns 127.0.0.2 only → Score: 10 (matches first rule)
  • If DNSBL returns 127.0.0.11 only → Score: 5 (matches second rule)
  • If DNSBL returns both 127.0.0.2 and 127.0.0.11 → Score: 15 (both rules match, scores sum)
  • If DNSBL returns both 127.0.0.2 and 127.0.0.3 → Score: 10 (same rule matches, counted once)

Backwards compatibility: When response blocks are not used, the legacy
responses and score directives work as before.


Reference Checks Milter (`docs/reference/checks/milter.md`)

Milter client

The 'milter' implements subset of Sendmail's milter protocol that can be used
to integrate external software with maddy.
maddy implements version 6 of the protocol, older versions are
not supported.

Notable limitations of protocol implementation in maddy include:

  1. Changes of envelope sender address are not supported
  2. Removal and addition of envelope recipients is not supported
  3. Removal and replacement of header fields is not supported
  4. Headers fields can be inserted only on top
  5. Milter does not receive some "macros" provided by sendmail.

Restrictions 1 and 2 are inherent to the maddy checks interface and cannot be
removed without major changes to it. Restrictions 3, 4 and 5 are temporary due to
incomplete implementation.

text
check.milter {
	endpoint <endpoint>
	fail_open false
}

milter <endpoint>

Arguments

When defined inline, the first argument specifies endpoint to access milter
via. See below.

Configuration directives

endpoint _scheme://path_

Default: not set

Specifies milter protocol endpoint to use.
The endpoit is specified in standard URL-like format:
tcp://127.0.0.1:6669 or unix:///var/lib/milter/filter.sock


fail_open _boolean_

Default: false

Toggles behavior on milter I/O errors. If false ("fail closed") - message is
rejected with temporary error code. If true ("fail open") - check is skipped.


Reference Checks Misc (`docs/reference/checks/misc.md`)

Misc checks

Configuration directives

Following directives are defined for all modules listed below.

fail_action `ignore` | `reject` | `quarantine`

Default: quarantine

Action to take when check fails. See Check actions for details.


debug _boolean_

Default: global directive value

Log both successful and unsuccessful check executions instead of just
unsuccessful.


require_mx_record

Check that domain in MAIL FROM command does have a MX record and none of them
are "null" (contain a single dot as the host).

By default, quarantines messages coming from servers missing MX records,
use fail_action directive to change that.


require_matching_rdns

Check that source server IP does have a PTR record point to the domain
specified in EHLO/HELO command.

By default, quarantines messages coming from servers with mismatched or missing
PTR record, use fail_action directive to change that.


require_tls

Check that the source server is connected via TLS; either directly, or by using
the STARTTLS command.

By default, rejects messages coming from unencrypted servers. Use the
fail_action directive to change that.


Reference Checks Rspamd (`docs/reference/checks/rspamd.md`)

rspamd

The 'rspamd' module implements message filtering by contacting the rspamd
server via HTTP API.

text
check.rspamd {
	tls_client { ... }
	api_path http://127.0.0.1:11333
	settings_id whatever
	tag maddy
	hostname mx.example.org
	io_error_action ignore
	error_resp_action ignore
	add_header_action quarantine
	rewrite_subj_action quarantine
	reject_action reject
	soft_reject_action reject
	flags pass_all
}

rspamd http://127.0.0.1:11333

Configuration directives

tls_client { ... }

Default: not set

Configure TLS client if HTTPS is used. See TLS configuration / Client for details.


api_path _url_

Default: http://127.0.0.1:11333

URL of HTTP API endpoint. Supports both HTTP and HTTPS and can include
path element.


settings_id _string_

Default: not set

Settings ID to pass to the server.


tag _string_

Default: maddy

Value to send in MTA-Tag header field.


hostname _string_

Default: value of global directive

Value to send in MTA-Name header field.


io_error_action _action_

Default: ignore

Action to take in case of inability to contact the rspamd server.


error_resp_action _action_

Default: ignore

Action to take in case of 5xx or 4xx response received from the rspamd server.


add_header_action _action_

Default: quarantine

Action to take when rspamd requests to "add header".

X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.


rewrite_subj_action _action_

Default: quarantine

Action to take when rspamd requests to "rewrite subject".

X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.


reject_action _action_

Default: reject

Action to take when rspamd requests to "reject".


soft_reject_action _action_

Default: reject

Action to take when rspamd requests to "soft reject".


flags _string-list..._

Default: pass_all

Flags to pass to the rspamd server.
See https://rspamd.com/doc/architecture/protocol.html for details.


Reference Checks Spf (`docs/reference/checks/spf.md`)

SPF

check.spf the check module that verifies whether IP address of the client is
authorized to send messages for domain in MAIL FROM address.

SPF statuses are mapped to maddy check actions in a way
specified by *_action directives. By default, SPF failure
results in the message being quarantined and errors (both permanent and
temporary) cause message to be rejected.
Authentication-Results field is generated irregardless of status.

DMARC override

It is recommended by the DMARC standard to don't fail delivery based solely on
SPF policy and always check DMARC policy and take action based on it.

If enforce_early is no, check.spf module will not take any action on SPF
policy failure if sender domain does have a DMARC record with 'quarantine' or
'reject' policy. Instead it will rely on DMARC support to take necesary
actions using SPF results as an input.

Disabling enforce_early without enabling DMARC support will make SPF policies
no-op and is considered insecure.

Configuration directives

text
check.spf {
    debug no
    enforce_early no
    fail_action quarantine
    softfail_action ignore
    permerr_action reject
    temperr_action reject
}

debug _boolean_

Default: global directive value

Enable verbose logging for check.spf.


enforce_early _boolean_

Default: no

Make policy decision on MAIL FROM stage (before the message body is received).
This makes it impossible to apply DMARC override (see above).


none_action `reject` | `quarantine` | `ignore`

Default: ignore

Action to take when SPF policy evaluates to a 'none' result.

See https://tools.ietf.org/html/rfc7208#section-2.6 for meaning of
SPF results.


neutral_action `reject` | `quarantine` | `ignore`

Default: ignore

Action to take when SPF policy evaluates to a 'neutral' result.

See https://tools.ietf.org/html/rfc7208#section-2.6 for meaning of
SPF results.


fail_action `reject` | `quarantine` | `ignore`

Default: quarantine

Action to take when SPF policy evaluates to a 'fail' result.


softfail_action `reject` | `quarantine` | `ignore`

Default: ignore

Action to take when SPF policy evaluates to a 'softfail' result.


permerr_action `reject` | `quarantine` | `ignore`

Default: reject

Action to take when SPF policy evaluates to a 'permerror' result.


temperr_action `reject` | `quarantine` | `ignore`

Default: reject

Action to take when SPF policy evaluates to a 'temperror' result.


Reference Endpoints Imap (`docs/reference/endpoints/imap.md`)

IMAP4rev1 endpoint

Module 'imap' is a listener that implements IMAP4rev1 protocol and provides
access to local messages storage specified by 'storage' directive.

In most cases, local storage modules will auto-create accounts when they are
accessed via IMAP. This relies on authentication provider used by IMAP endpoint
to provide what essentially is access control. There is a caveat, however: this
auto-creation will not happen when delivering incoming messages via SMTP as
there is no authentication to confirm that this account should indeed be
created.

Configuration directives

text
imap tcp://0.0.0.0:143 tls://0.0.0.0:993 {
    tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
    io_debug no
    debug no
    insecure_auth no
    sasl_login no
    auth pam
    storage &local_mailboxes
    auth_map identity
    auth_map_normalize auto
    storage_map identity
    storage_map_normalize auto
}

tls _certificate-path_ _key-path_ { ... }

Default: global directive value

TLS certificate & key to use. Fine-tuning of other TLS properties is possible
by specifying a configuration block and options inside it:

text
tls cert.crt key.key {
    protocols tls1.2 tls1.3
}

See TLS configuration / Server for details.


proxy_protocol _trusted ips..._ { ... }

Default: not enabled

Enable use of HAProxy PROXY protocol. Supports both v1 and v2 protocols.
If a list of trusted IP addresses or subnets is provided, only connections
from those will be trusted.

TLS for the channel between the proxies and maddy can be configured
using a 'tls' directive:

text
proxy_protocol {
    trust 127.0.0.1 ::1 192.168.0.1/24
    tls &proxy_tls
}

Note that the top-level 'tls' directive is not inherited here. If you
need TLS on top of the PROXY protocol, securing the protocol header,
you must declare TLS explicitly.


io_debug _boolean_

Default: no

Write all commands and responses to stderr.


io_errors _boolean_

Default: no

Log I/O errors.


debug _boolean_

Default: global directive value

Enable verbose logging.


insecure_auth _boolean_

Default: no (yes if TLS is disabled)

Allow plain-text authentication over unencrypted connections.


sasl_login _boolean_

Default: no

Enable support for SASL LOGIN authentication mechanism used by
some outdated clients.


auth _module-reference_

Required.

Use the specified module for authentication.


storage _module-reference_

Required.

Use the specified module for message storage.


storage_map _module-reference_

Default: identity

Use the specified table to map SASL usernames to storage account names.

Before username is looked up, it is normalized using function defined by
storage_map_normalize.

This directive is useful if you want users [email protected] and [email protected]
to share the same storage account named "user". In this case, use

text
storage_map email_localpart

Note that storage_map does not affect the username passed to the
authentication provider.

It also does not affect how message delivery is handled, you should specify
delivery_map in storage module to define how to map email addresses
to storage accounts. E.g.

text
storage.imapsql local_mailboxes {
        ...
        delivery_map email_localpart # deliver "user@*" to mailbox for "user"
    }

storage_map_normalize _function_

Default: auto

Same as auth_map_normalize but for storage_map.


auth_map_normalize _function_

Default: auto

Overrides global auth_map_normalize value for this endpoint.

See Global configuration for details.


Reference Endpoints Openmetrics (`docs/reference/endpoints/openmetrics.md`)

OpenMetrics/Prometheus telemetry

Various server statistics are provided in OpenMetrics format by the
"openmetrics" module.

To enable it, add the following line to the server config:

text
openmetrics tcp://127.0.0.1:9749 { }

Scrape endpoint would be http://127.0.0.1:9749/metrics.

Metrics

text
# AUTH command failures due to invalid credentials.
maddy_smtp_failed_logins{module}
# Failed SMTP transaction commands (MAIL, RCPT, DATA).
maddy_smtp_failed_commands{module, command, smtp_code, smtp_enchcode}
# Messages rejected with 4xx code due to ratelimiting.
maddy_smtp_ratelimit_deferred{module}
# Amount of started SMTP transactions started.
maddy_smtp_started_transactions{module}
# Amount of aborted SMTP transactions started.
maddy_smtp_aborted_transactions{module}
# Amount of completed SMTP transactions.
maddy_smtp_completed_transactions{module}
# Number of times a check returned 'reject' result (may be more than processed
# messages if check does so on per-recipient basis).
maddy_check_reject{check}
# Number of times a check returned 'quarantine' result (may be more than
# processed messages if check does so on per-recipient basis).
maddy_check_quarantined{check}
# Amount of queued messages.
maddy_queue_length{module, location}
# Outbound connections established with specific TLS security level.
maddy_remote_conns_tls_level{module, level}
# Outbound connections established with specific MX security level.
maddy_remote_conns_mx_level{module, level}

Reference Endpoints Smtp (`docs/reference/endpoints/smtp.md`)

SMTP/LMTP/Submission endpoint

Module 'smtp' is a listener that implements ESMTP protocol with optional
authentication, LMTP and Submission support. Incoming messages are processed in
accordance with pipeline rules (explained in Message pipeline section below).

text
smtp tcp://0.0.0.0:25 {
    hostname example.org
    tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
    io_debug no
    debug no
    insecure_auth no
    sasl_login no
    read_timeout 10m
    write_timeout 1m
    shutdown_timeout 3m
    max_message_size 32M
    max_header_size 1M
    auth pam
    defer_sender_reject yes
    dmarc yes
    smtp_max_line_length 4000
    limits {
        endpoint rate 10
        endpoint concurrency 500
    }

    # Example pipeline configuration.
    destination example.org {
        deliver_to &local_mailboxes
    }
    default_destination {
        reject
    }
}

Configuration directives

hostname _string_

Default: global directive value

Server name to use in SMTP banner.

text
220 example.org ESMTP Service Ready

tls _certificate-path_ _key-path_ { ... }

Default: global directive value

TLS certificate & key to use. Fine-tuning of other TLS properties is possible
by specifying a configuration block and options inside it:

text
tls cert.crt key.key {
    protocols tls1.2 tls1.3
}

See TLS configuration / Server for details.


proxy_protocol _trusted ips..._ { ... }

Default: not enabled

Enable use of HAProxy PROXY protocol. Supports both v1 and v2 protocols.
If a list of trusted IP addresses or subnets is provided, only connections
from those will be trusted.

TLS for the channel between the proxies and maddy can be configured
using a 'tls' directive:

text
proxy_protocol {
    trust 127.0.0.1 ::1 192.168.0.1/24
    tls &proxy_tls
}

io_debug _boolean_

Default: no

Write all commands and responses to stderr.


debug _boolean_

Default: global directive value

Enable verbose logging.


insecure_auth _boolean_

Default: no (yes if TLS is disabled)

Allow plain-text authentication over unencrypted connections. Not recommended!


sasl_login _boolean_

Default: no

Enable support for SASL LOGIN authentication mechanism used by
some outdated clients.


read_timeout _duration_

Default: 10m

I/O read timeout.


write_timeout _duration_

Default: 1m

I/O write timeout.


shutdown_timeout _duration_

Default: 3m

Time to wait until forcibly closing connections on server shutdown
or configuration reload.


max_message_size _size_

Default: 32M

Limit the size of incoming messages to 'size'.


max_header_size _size_

Default: 1M

Limit the size of incoming message headers to 'size'.


auth _module-reference_

Default: not specified

Use the specified module for authentication.


defer_sender_reject _boolean_

Default: yes

Apply sender-based checks and routing logic when first RCPT TO command
is received. This allows maddy to log recipient address of the rejected
message and also improves interoperability with (improperly implemented)
clients that don't expect an error early in session.


max_logged_rcpt_errors _integer_

Default: 5

Amount of RCPT-time errors that should be logged. Further errors will be
handled silently. This is to prevent log flooding during email dictionary
attacks (address probing).


max_received _integer_

Default: 50

Max. amount of Received header fields in the message header. If the incoming
message has more fields than this number, it will be rejected with the permanent error
5.4.6 ("Routing loop detected").


buffer `ram`buffer `fs` _path_ buffer `auto` _max-size_ _path_

Default: auto 1M StateDirectory/buffer

Temporary storage to use for the body of accepted messages.

  • ram – Store the body in RAM.
  • fs – Write out the message to the FS and read it back as needed.
    path can be omitted and defaults to StateDirectory/buffer.
  • auto – Store message bodies smaller than _max_size_ entirely in RAM,
    otherwise write them out to the FS. path can be omitted and defaults to StateDirectory/buffer.

smtp_max_line_length _integer_

Default: 4000

The maximum line length allowed in the SMTP input stream. If client sends a
longer line - connection will be closed and message (if any) will be rejected
with a permanent error.

RFC 5321 has the recommended limit of 998 bytes. Servers are not required
to handle longer lines correctly but some senders may produce them.

Unless BDAT extension is used by the sender, this limitation also applies to
the message body.


dmarc _boolean_

Default: yes

Enforce sender's DMARC policy. Due to implementation limitations, it is not a
check module.

Note: Report generation is not implemented now.

Note: DMARC needs SPF and DKIM checks to function correctly.
Without these, DMARC check will not run.


Rate & concurrency limiting

limits { ... }

Default: no limits

This allows configuring a set of message flow restrictions including
max. concurrency and rate per-endpoint, per-source, per-destination.

Limits are specified as directives inside the block:

text
limits {
	all rate 20
	destination concurrency 5
}

Supported limits:

_scope_ rate _burst_ _period_

Rate limit. Restrict the amount of messages processed in period to
burst messages. If period is not specified, 1 second is used.

_scope_ concurrency _max_

Concurrency limit. Restrict the amount of messages processed in parallel
to max.

For each supported limitation, scope determines whether it should be applied
for all messages ("all"), per-sender IP ("ip"), per-sender domain ("source") or
per-recipient domain ("destination"). Having a scope other than "all" means
that the restriction will be enforced independently for each group determined
by scope. E.g. "ip rate 20" means that the same IP cannot send more than 20
messages per second. "destination concurrency 5" means that no more than 5
messages can be sent in parallel to a single domain.

Note: At the moment, SMTP endpoint on its own does not support per-recipient
limits. They will be no-op. If you want to enforce a per-recipient restriction
on outbound messages, do so using 'limits' directive for the 'table.remote' module

It is possible to share limit counters between multiple endpoints (or any other
modules). To do so define a top-level configuration block for module "limits"
and reference it where needed using standard & syntax. E.g.

text
limits inbound_limits {
	all rate 20
}

smtp smtp://0.0.0.0:25 {
	limits &inbound_limits
	...
}

submission tls://0.0.0.0:465 {
	limits &inbound_limits
	...
}

Using an "all rate" restriction in such way means that no more than 20
messages can enter the server through both endpoints in one second.

Submission module (submission)

Module 'submission' implements all functionality of the 'smtp' module and adds
certain message preprocessing on top of it, additionally authentication is
always required.

'submission' module checks whether addresses in header fields From, Sender, To,
Cc, Bcc, Reply-To are correct and adds Message-ID and Date if it is missing.

text
submission tcp://0.0.0.0:587 tls://0.0.0.0:465 {
    # ... same as smtp ...
}

LMTP module (lmtp)

Module 'lmtp' implements all functionality of the 'smtp' module but uses
LMTP (RFC 2033) protocol.

text
lmtp unix://lmtp.sock {
    # ... same as smtp ...
}

Limitations of LMTP implementation

  • Can't be used with TCP.
  • Delivery to 'sql' module storage is always atomic, either all recipients will
    succeed or none of them will.

Reference Modifiers Dkim (`docs/reference/modifiers/dkim.md`)

DKIM signing

modify.dkim module is a modifier that signs messages using DKIM
protocol (RFC 6376).

Each configuration block specifies a single selector
and one or more domains.

A key will be generated or read for each domain, the key to use
for each message will be selected based on the SMTP envelope sender. Exception
for that is that for domain-less postmaster address and null address, the
key for the first domain will be used. If domain in envelope sender
does not match any of loaded keys, message will not be signed.
Additionally, for each messages From header is checked to
match MAIL FROM and authorization identity (username sender is logged in as).
This can be controlled using require_sender_match directive.

Generated private keys are stored in unencrypted PKCS#8 format
in state_directory/dkim_keys (/var/lib/maddy/dkim_keys).
In the same directory .dns files are generated that contain
public key for each domain formatted in the form of a DNS record.

Arguments

domains and selector can be specified in arguments, so actual modify.dkim use can
be shortened to the following:

text
modify {
    dkim example.org selector
}

Configuration directives

text
modify.dkim {
    debug no
    domains example.org example.com
    selector default
    key_path dkim-keys/{domain}-{selector}.key
    oversign_fields ...
    sign_fields ...
    header_canon relaxed
    body_canon relaxed
    sig_expiry 120h # 5 days
    hash sha256
    newkey_algo rsa2048
}

debug _boolean_

Default: global directive value

Enable verbose logging.


domains _string-list_

Required.
Default: not specified

ADministrative Management Domains (ADMDs) taking responsibility for messages.

Should be specified either as a directive or as an argument.


selector _string_

Required.
Default: not specified

Identifier of used key within the ADMD.
Should be specified either as a directive or as an argument.


key_path _string_

Default: dkim_keys/{domain}_{selector}.key

Path to private key. It should be in PKCS#8 format wrapped in PAM encoding.
If key does not exist, it will be generated using algorithm specified
in newkey_algo.

Placeholders '{domain}' and '{selector}' will be replaced with corresponding
values from domain and selector directives.

Additionally, keys in PKCS#1 ("RSA PRIVATE KEY") and
RFC 5915 ("EC PRIVATE KEY") can be read by modify.dkim. Note, however that
newly generated keys are always in PKCS#8.


oversign_fields _list..._

Default: see below

Header fields that should be signed n+1 times where n is times they are
present in the message. This makes it impossible to replace field
value by prepending another field with the same name to the message.

Fields specified here don't have to be also specified in sign_fields.

Default set of oversigned fields:

  • Subject
  • To
  • From
  • Date
  • MIME-Version
  • Content-Type
  • Content-Transfer-Encoding
  • Reply-To
  • Message-Id
  • References
  • Autocrypt
  • Openpgp

sign_fields _list..._

Default: see below

Header fields that should be signed n times where n is times they are
present in the message. For these fields, additional values can be prepended
by intermediate relays, but existing values can't be changed.

Default set of signed fields:

  • List-Id
  • List-Help
  • List-Unsubscribe
  • List-Post
  • List-Owner
  • List-Archive
  • Resent-To
  • Resent-Sender
  • Resent-Message-Id
  • Resent-Date
  • Resent-From
  • Resent-Cc

header_canon `relaxed` | `simple`

Default: relaxed

Canonicalization algorithm to use for header fields. With relaxed, whitespace within
fields can be modified without breaking the signature, with simple no
modifications are allowed.


body_canon `relaxed` | `simple`

Default: relaxed

Canonicalization algorithm to use for message body. With relaxed, whitespace within
can be modified without breaking the signature, with simple no
modifications are allowed.


sig_expiry _duration_

Default: 120h

Time for which signature should be considered valid. Mainly used to prevent
unauthorized resending of old messages.


hash _hash_

Default: sha256

Hash algorithm to use when computing body hash.

sha256 is the only supported algorithm now.


newkey_algo `rsa4096` | `rsa2048` | `ed25519`

Default: rsa2048

Algorithm to use when generating a new key.

Currently ed25519 is not supported by most platforms.


require_sender_match _ids..._

Default: envelope auth

Require specified identifiers to match From header field and key domain,
otherwise - don't sign the message.

If From field contains multiple addresses, message will not be
signed unless allow_multiple_from is also specified. In that
case only first address will be compared.

Matching is done in a case-insensitive way.

Valid values:

  • off – Disable check, always sign.
  • envelope – Require MAIL FROM address to match From header.
  • auth – If authorization identity contains @ - then require it to
    fully match From header. Otherwise, check only local-part
    (username).

allow_multiple_from _boolean_

Default: no

Allow multiple addresses in From header field for purposes of
require_sender_match checks. Only first address will be checked, however.


sign_subdomains _boolean_

Default: no

Sign emails from subdomains using a top domain key.

Allows only one domain to be specified (can be worked around by using modify.dkim
multiple times).


Reference Modifiers Envelope (`docs/reference/modifiers/envelope.md`)

Envelope sender / recipient rewriting

replace_sender and replace_rcpt modules replace SMTP envelope addresses
based on the mapping defined by the table module (maddy-tables(5)). It is possible
to specify 1:N mappings. This allows, for example, implementing mailing lists.

The address is normalized before lookup (Punycode in domain-part is decoded,
Unicode is normalized to NFC, the whole string is case-folded).

First, the whole address is looked up. If there is no replacement, local-part
of the address is looked up separately and is replaced in the address while
keeping the domain part intact. Replacements are not applied recursively, that
is, lookup is not repeated for the replacement.

Recipients are not deduplicated after expansion, so message may be delivered
multiple times to a single recipient. However, used delivery target can apply
such deduplication (imapsql storage does it).

Definition:

text
replace_rcpt <table> [table arguments] {
	[extended table config]
}
replace_sender <table> [table arguments] {
	[extended table config]
}

Use examples:

text
modify {
	replace_rcpt file /etc/maddy/aliases
	replace_rcpt static {
		entry [email protected] [email protected]
		entry [email protected] [email protected] [email protected]
	}
	replace_rcpt regexp "(.+)@example.net" "[email protected]"
	replace_rcpt regexp "(.+)@example.net" "[email protected]" "[email protected]"
}

Possible contents of /etc/maddy/aliases in the example above:

text
# Replace 'cat' with any domain to 'dog'.
# E.g. [email protected] -> [email protected]
cat: dog

# Replace [email protected] with [email protected].
# Takes priority over the previous line.
[email protected]: [email protected]

# Using aliases in multiple lines
cat2: dog
cat2: mouse
[email protected]: [email protected]
[email protected]: [email protected]
# Comma-separated aliases in multiple lines
cat3: dog , mouse
[email protected]: [email protected] , [email protected]

Reference Storage Imap Filters (`docs/reference/storage/imap-filters.md`)

IMAP filters

Most storage backends support application of custom code late in delivery
process. As opposed to using SMTP pipeline modifiers or checks, it allows
modifying IMAP-specific message attributes. In particular, it allows
code to change target folder and add IMAP flags (keywords) to the message.

There is no way to reject message using IMAP filters, this should be done
earlier in SMTP pipeline logic. Quarantined messages are not processed
by IMAP filters and are unconditionally delivered to Junk folder (or other
folder with \Junk special-use attribute).

To use an IMAP filter, specify it in the 'imap_filter' directive for the
used storage backend, like this:

text
storage.imapsql local_mailboxes {
   ...
   
   imap_filter {
       command /etc/maddy/sieve.sh {account_name}
   }
}

System command filter (imap.filter.command)

This filter is similar to check.command module
and runs a system command to obtain necessary information.

Usage:

text
command executable_name args... { }

Same as check.command, following placeholders are supported for command
arguments: {source_ip}, {source_host}, {source_rdns}, {msg_id}, {auth_user},
{sender}. Note: placeholders
in command name are not processed to avoid possible command injection attacks.

Additionally, for imap.filter.command, {account_name} placeholder is replaced
with effective IMAP account name, {rcpt_to}, {original_rcpt_to} provide
access to the SMTP envelope recipient (before and after any rewrites),
{subject} is replaced with the Subject header, if it is present.

Note that if you use provided systemd units on Linux, maddy executable is
sandboxed - all commands will be executed with heavily restricted filesystem
access and other privileges. Notably, /tmp is isolated and all directories
except for /var/lib/maddy and /run/maddy are read-only. You will need to modify
systemd unit if your command needs more privileges.

Command output should consist of zero or more lines. First one, if non-empty, overrides
destination folder. All other lines contain additional IMAP flags to add
to the message. If command wants to add flags without changing folder - first
line should be empty.

It is valid for command to not write anything to stdout. In this case its
execution will have no effect on delivery.

Output example:

text
Junk

In this case, message will be placed in the Junk folder.

text
$Label1

In this case, message will be placed in inbox and will have
'$Label1' added.


Reference Storage Imapsql (`docs/reference/storage/imapsql.md`)

SQL-indexed storage

The imapsql module implements database for IMAP index and message
metadata using SQL-based relational database.

Message contents are stored in an "blob store" defined by msg_store
directive. By default this is a file system directory under /var/lib/maddy.

Supported RDBMS:

  • SQLite 3.25.0
  • PostgreSQL 9.6 or newer
  • CockroachDB 20.1.5 or newer

Account names are required to have the form of a email address (unless configured otherwise)
and are case-insensitive. UTF-8 names are supported with restrictions defined in the
PRECIS UsernameCaseMapped profile.

text
storage.imapsql {
	driver sqlite3
	dsn imapsql.db
	msg_store fs messages/
}

imapsql module also can be used as a lookup table.
It returns empty string values for existing usernames. This might be useful
with destination_in directive e.g. to implement catch-all
addresses (this is a bad idea to do so, this is just an example):

text
destination_in &local_mailboxes {
	deliver_to &local_mailboxes
}
destination example.org {
	modify {
		replace_rcpt regexp ".*" "[email protected]"
	}
	deliver_to &local_mailboxes
}

Arguments

Specify the driver and DSN.

Configuration directives

driver _string_

Required.
Default: not specified

Use a specified driver to communicate with the database. Supported values:
sqlite3, postgres.

Should be specified either via an argument or via this directive.


dsn _string_

Required.
Default: not specified

Data Source Name, the driver-specific value that specifies the database to use.

For SQLite3 this is just a file path.
For PostgreSQL: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters

Should be specified either via an argument or via this directive.


msg_store _store_

Default: fs messages/

Module to use for message bodies storage.

See "Blob storage" section for what you can use here.


compression `off`compression _algorithm_compression _algorithm_ _level_

Default: off

Apply compression to message contents.
Supported algorithms: lz4, zstd.


appendlimit _size_

Default: 32M

Don't allow users to add new messages larger than 'size'.

This does not affect messages added when using module as a delivery target.
Use max_message_size directive in SMTP endpoint module to restrict it too.


debug _boolean_

Default: global directive value

Enable verbose logging.


junk_mailbox _name_

Default: Junk

The folder to put quarantined messages in. Thishis setting is not used if user
does have a folder with "Junk" special-use attribute.


disable_recent _boolean_

Default: true

Disable RFC 3501-conforming handling of \Recent flag.

This significantly improves storage performance when SQLite3 or CockroackDB is
used at the cost of confusing clients that use this flag.


sqlite_cache_size _integer_

Default: defined by SQLite

SQLite page cache size. If positive - specifies amount of pages (1 page - 4
KiB) to keep in cache. If negative - specifies approximate upper bound
of cache size in KiB.


sqlite_busy_timeout _integer_

Default: 5000000

SQLite-specific performance tuning option. Amount of milliseconds to wait
before giving up on DB lock.


imap_filter { ... }

Default: not set

Specifies IMAP filters to apply for messages delivered from SMTP pipeline.

Ex.

text
imap_filter {
	command /etc/maddy/sieve.sh {account_name}
}

delivery_map _table_

Default: identity

Use specified table module to map recipient
addresses from incoming messages to mailbox names.

Normalization algorithm specified in delivery_normalize is appied before
delivery_map.


delivery_normalize _name_

Default: precis_casefold_email

Normalization function to apply to email addresses before mapping them
to mailboxes.

See auth_normalize.


auth_map _table_

Deprecated: Use storage_map in imap config instead.
Default: identity

Use specified table module to map authentication
usernames to mailbox names.

Normalization algorithm specified in auth_normalize is applied before
auth_map.


auth_normalize _name_

Deprecated: Use storage_map_normalize in imap config instead.
Default: precis_casefold_email

Normalization function to apply to authentication usernames before mapping
them to mailboxes.

Available options:

  • precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain
  • precis_casefold PRECIS UsernameCaseMapped profile for the entire string
  • precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain
  • precis PRECIS UsernameCasePreserved profile for the entire string
  • casefold Convert to lower case
  • noop Nothing

Note: On message delivery, recipient address is unconditionally normalized
using precis_casefold_email function.


Reference Table Auth (`docs/reference/table/auth.md`)

Authentication providers

Most authentication providers are also usable as a table
that contains all usernames known to the module. Exceptions are auth.external and
pam as underlying interfaces do not define a way to check credentials
existence.


Reference Table Chain (`docs/reference/table/chain.md`)

Table chaining

The table.chain module allows chaining together multiple table modules
by using value returned by a previous table as an input for the second
table.

Example:

text
table.chain {
	step regexp "(.+)(\\+[^+"@]+)[email protected]" "[email protected]"
	step file /etc/maddy/emails
}

This will strip +prefix from mailbox before looking it up
in /etc/maddy/emails list.

Configuration directives

step _table_

Adds a table module to the chain. If input value is not in the table
(e.g. file) - return "not exists" error.


optional_step _table_

Same as step but if input value is not in the table - it is passed to the
next step without changes.

Example:
Something like this can be used to map emails to usernames
after translating them via aliases map:

text
table.chain {
    optional_step file /etc/maddy/aliases
    step email_localpart
}

Reference Table Email Localpart (`docs/reference/table/email_localpart.md`)

Email local part

The module table.email_localpart extracts and unescapes local ("username") part
of the email address.

E.g.

Mappings for invalid emails are not defined (will be treated as non-existing
values).

text
table.email_localpart { }

table.email_localpart_optional works the same, but returns non-email strings
as is. This can be used if you want to accept both [email protected] and
user somewhere and treat it the same.


Reference Table Email With Domain (`docs/reference/table/email_with_domain.md`)

Email with domain

The table module table.email_with_domain appends one or more
domains (allowing 1:N expansion) to the specified value.

text
table.email_with_domain DOMAIN DOMAIN... { }

It can be used to implement domain-level expansion for aliases if used together
with table.chain. Example:

text
modify {
    replace_rcpt chain {
        step email_local_part
        step email_with_domain example.org example.com
    }
}

This configuration will alias anything@anydomain to [email protected]
and [email protected].

It is also useful with authorize_sender to authorize sending using multiple
addresses under different domains if non-email usernames are used for
authentication:

text
check.authorize_sender {
   ...
   user_to_email email_with_domain example.org example.com
}

This way, user authenticated as user will be allowed to use
[email protected] or [email protected] as a sender address.


Reference Table File (`docs/reference/table/file.md`)

File

table.file module builds string-string mapping from a text file.

File is reloaded every 15 seconds if there are any changes (detected using
modification time). No changes are applied if file contains syntax errors.

Definition:

text
file <file path>

or

text
file {
	file <file path>
}

Usage example:

text
# Resolve SMTP address aliases using text file mapping.
modify {
	replace_rcpt file /etc/maddy/aliases
}

Syntax

Better demonstrated by examples:

text
# Lines starting with # are ignored.

# And so are lines only with whitespace.

# Whenever 'aaa' is looked up, return 'bbb'
aaa: bbb

	# Trailing and leading whitespace is ignored.
	ccc: ddd

# If there is no colon, the string is translated into ""
# That is, the following line is equivalent to
#	aaa:
aaa

# If the same key is used multiple times - table.file will return
# multiple values when queries.
ddd: firstvalue
ddd: secondvalue

# Alternatively, multiple values can be specified
# using a comma. There is no support for escaping
# so you would have to use a different format if you require
# comma-separated values.
ddd: firstvalue, secondvalue

Reference Table Regexp (`docs/reference/table/regexp.md`)

Regexp rewrite table

The 'regexp' module implements table lookups by applying a regular expression
to the key value. If it matches - 'replacement' value is returned with $N
placeholders being replaced with corresponding capture groups from the match.
Otherwise, no value is returned.

The regular expression syntax is the subset of PCRE. See
https://golang.org/pkg/regexp/syntax/ for details.

text
table.regexp <regexp> [replacement] {
	full_match yes
	case_insensitive yes
	expand_placeholders yes
}

Note that [replacement] is optional. If it is not included - table.regexp
will return the original string, therefore acting as a regexp match check.
This can be useful in combination in destination_in for
advanced matching:

text
destination_in regexp ".*-bounce+.*@example.com" {
	...
}

Configuration directives

full_match _boolean_

Default: yes

Whether to implicitly add start/end anchors to the regular expression.
That is, if full_match is yes, then the provided regular expression should
match the whole string. With no - partial match is enough.


case_insensitive _boolean_

Default: yes

Whether to make matching case-insensitive.


expand_placeholders _boolean_

Default: yes

Replace '$name' and '${name}' in the replacement string with contents of
corresponding capture groups from the match.

To insert a literal $ in the output, use $$ in the template.

Identity table (table.identity)

The module 'identity' is a table module that just returns the key looked up.

text
table.identity { }

Reference Table Sql Query (`docs/reference/table/sql_query.md`)

SQL query mapping

The table.sql_query module implements table interface using SQL queries.

Definition:

text
table.sql_query {
	driver <driver name>
	dsn <data source name>
	lookup <lookup query>

	# Optional:
	init <init query list>
	list <list query>
	add <add query>
	del <del query>
	set <set query>
}

Usage example:

text
# Resolve SMTP address aliases using PostgreSQL DB.
modify {
	replace_rcpt sql_query {
		driver postgres
		dsn "dbname=maddy user=maddy"
		lookup "SELECT alias FROM aliases WHERE address = $1"
	}
}

Configuration directives

driver _driver name_

Required.

Driver to use to access the database.

Supported drivers: postgres, sqlite3 (if compiled with C support)


dsn _data source name_

Required.

Data Source Name to pass to the driver. For SQLite3 this is just a path to DB
file. For Postgres, see
https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection_String_Parameters


lookup _query_

Required.

SQL query to use to obtain the lookup result.

It will get one named argument containing the lookup key. Use :key
placeholder to access it in SQL. The result row set should contain one row, one
column with the string that will be used as a lookup result. If there are more
rows, they will be ignored. If there are more columns, lookup will fail. If
there are no rows, lookup returns "no results". If there are any error - lookup
will fail.


init _queries..._

Default: empty

List of queries to execute on initialization. Can be used to configure RDBMS.

Example, to improve SQLite3 performance:

text
table.sql_query {
	driver sqlite3
	dsn whatever.db
	init "PRAGMA journal_mode=WAL" \
		"PRAGMA synchronous=NORMAL"
	lookup "SELECT alias FROM aliases WHERE address = $1"
}

named_args _boolean_

Default: yes

Whether to use named parameters binding when executing SQL queries
or not.

Note that maddy's PostgreSQL driver does not support named parameters and
SQLite3 driver has issues handling numbered parameters:
https://github.com/mattn/go-sqlite3/issues/472


add _query_list _query_set _query_ del _query_

Default: none

If queries are set to implement corresponding table operations - table becomes
"mutable" and can be used in contexts that require writable key-value store.

'add' query gets :key, :value named arguments - key and value strings to store.
They should be added to the store. The query should not add multiple values
for the same key and should fail if the key already exists.

'list' query gets no arguments and should return a column with all keys in
the store.

'set' query gets :key, :value named arguments - key and value and should replace the existing
entry in the database.

'del' query gets :key argument - key and should remove it from the database.

If named_args is set to no - key is passed as the first numbered parameter
($1), value is passed as the second numbered parameter ($2).


Reference Table Static (`docs/reference/table/static.md`)

Static table

The 'static' module implements table lookups using key-value pairs in its
configuration.

text
table.static {
	entry KEY1 VALUE1
	entry KEY2 VALUE2
	...
}

Configuration directives

entry _key_ _value_

Add an entry to the table.

If the same key is used multiple times, the last one takes effect.


Reference Targets Queue (`docs/reference/targets/queue.md`)

Local queue

Queue module buffers messages on disk and retries delivery multiple times to
another target to ensure reliable delivery.

It is also responsible for generation of DSN messages
in case of delivery failures.

Arguments

First argument specifies directory to use for storage.
Relative paths are relative to the StateDirectory.

Configuration directives

text
target.queue {
    target remote
    location ...
    max_parallelism 16
    max_tries 4
	bounce {
	    destination example.org {
	        deliver_to &local_mailboxes
	    }
	    default_destination {
	        reject
	    }
	}

    autogenerated_msg_domain example.org
    debug no
}

target _block_name_

Required.
Default: not specified

Delivery target to use for final delivery.


location _directory_

Default: StateDirectory/configuration_block_name

File system directory to use to store queued messages.
Relative paths are relative to the StateDirectory.


max_parallelism _integer_

Default: 16

Start up to integer goroutines for message processing. Basically, this option
limits amount of messages tried to be delivered concurrently.


max_tries _integer_

Default: 20

Attempt delivery up to integer times. Note that no more attempts will be done
is permanent error occurred during previous attempt.

Delay before the next attempt will be increased exponentially using the
following formula: 15mins * 1.2 ^ (n - 1) where n is the attempt number.
This gives you approximately the following sequence of delays:
18mins, 21mins, 25mins, 31mins, 37mins, 44mins, 53mins, 64mins, ...


bounce { ... }

Default: not specified

This configuration contains pipeline configuration to be used for generated DSN
(Delivery Status Notification) messages.

If this is block is not present in configuration, DSNs will not be generated.
Note, however, this is not what you want most of the time.


autogenerated_msg_domain _domain_

Default: global directive value

Domain to use in sender address for DSNs. Should be specified too if 'bounce'
block is specified.


debug _boolean_

Default: no

Enable verbose logging.


Reference Targets Remote (`docs/reference/targets/remote.md`)

Remote MX delivery

Module that implements message delivery to remote MTAs discovered via DNS MX
records. You probably want to use it with queue module for reliability.

If a message check marks a message as 'quarantined', remote module
will refuse to deliver it.

Configuration directives

text
target.remote {
    hostname mx.example.org
    debug no
}

hostname _domain_

Default: global directive value

Hostname to use client greeting (EHLO/HELO command). Some servers require it to
be FQDN, SPF-capable servers check whether it corresponds to the server IP
address, so it is better to set it to a domain that resolves to the server IP.


limits { ... }

Default: no limits

See 'limits' directive for SMTP endpoint.
It works the same except for address domains used for
per-source/per-destination are as observed when message exits the server.


local_ip _ip-address_

Default: empty

Choose the local IP to bind for outbound SMTP connections.


force_ipv4 _boolean_

Default: false

Force resolving outbound SMTP domains to IPv4 addresses. Some server providers
do not offer a way to properly set reverse PTR domains for IPv6 addresses; this
option makes maddy only connect to IPv4 addresses so that its public IPv4 address
is used to connect to that server, and thus reverse PTR checks are made against
its IPv4 address.

Warning: this may break sending outgoing mail to IPv6-only SMTP servers.


connect_timeout _duration_

Default: 5m

Timeout for TCP connection establishment.

RFC 5321 recommends 5 minutes for "initial greeting" that includes TCP
handshake. maddy uses two separate timers - one for "dialing" (DNS A/AAAA
lookup + TCP handshake) and another for "initial greeting". This directive
configures the former. The latter is not configurable and is hardcoded to be
5 minutes.


command_timeout _duration_

Default: 5m

Timeout for any SMTP command (EHLO, MAIL, RCPT, DATA, etc).

If STARTTLS is used this timeout also applies to TLS handshake.

RFC 5321 recommends 5 minutes for MAIL/RCPT and 3 minutes for
DATA.


submission_timeout _duration_

Default: 12m

Time to wait after the entire message is sent (after "final dot").

RFC 5321 recommends 10 minutes.


debug _boolean_

Default: global directive value

Enable verbose logging.


requiretls_override _boolean_

Default: true

Allow local security policy to be disabled using 'TLS-Required' header field in
sent messages. Note that the field has no effect if transparent forwarding is
used, message body should be processed before outbound delivery starts for it
to take effect (e.g. message should be queued using 'queue' module).

If true - all policies in mx_auth are no-op for a message with
TLS-Required: No in message header.


relaxed_requiretls _boolean_

Default: true

This option disables strict conformance with REQUIRETLS specification and
allows forwarding of messages 'tagged' with REQUIRETLS to MXes that are not
advertising REQUIRETLS support. It is meant to allow REQUIRETLS use without the
need to have support from all servers. It is based on the assumption that
server referenced by MX record is likely the final destination and therefore
there is only need to secure communication towards it and not beyond.


conn_reuse_limit _integer_

Default: 10

Amount of times the same SMTP connection can be used.
Connections are never reused if the previous DATA command failed.


conn_max_idle_count _integer_

Default: 10

Max. amount of idle connections per recipient domains to keep in cache.


conn_max_idle_time _integer_

Default: 150 (2.5 min)

Amount of time the idle connection is still considered potentially usable.


Security policies

mx_auth { ... }

Default: no policies

'remote' module implements a number of of schemes and protocols necessary to
ensure security of message delivery. Most of these schemes are concerned with
authentication of recipient server and TLS enforcement.

To enable mechanism, specify its name in the mx_auth directive block:

text
mx_auth {
	dane
	mtasts
}

Additional configuration is possible if supported by the mechanism by
specifying additional options as a block for the corresponding mechanism.
E.g.

text
mtasts {
	cache ram
}

If the mx_auth directive is not specified, no mechanisms are enabled. Note
that, however, this makes outbound SMTP vulnerable to a numerous downgrade
attacks and hence not recommended.

It is possible to share the same set of policies for multiple 'remote' module
instances by defining it at the top-level using mx_auth module and then
referencing it using standard & syntax:

text
mx_auth outbound_policy {
	dane
	mtasts {
		cache ram
	}
}

# ... somewhere else ...

deliver_to remote {
	mx_auth &outbound_policy
}

# ... somewhere else ...

deliver_to remote {
	mx_auth &outbound_policy
	tls_client { ... }
}

Note: If requiretls_override is true (default) - authenticated Submission sender
can disable entire mx_auth directive for a message by specifying
TLS-Required: No header field.


MTA-STS

Checks MTA-STS policy of the recipient domain. Provides proper authentication
and TLS enforcement for delivery, but partially vulnerable to persistent active
attacks.

Sets MX level to "mtasts" if the used MX matches MTA-STS policy even if it is
not set to "enforce" mode.

text
mtasts {
	cache fs
	fs_dir StateDirectory/mtasts_cache
}

cache `fs` | `ram`

Default: fs

Storage to use for MTA-STS cache. 'fs' is to use a filesystem directory, 'ram'
to store the cache in memory.

It is recommended to use 'fs' since that will not discard the cache (and thus
cause MTA-STS security to disappear) on server restart. However, using the RAM
cache can make sense for high-load configurations with good uptime.

fs_dir _directory_

Default: StateDirectory/mtasts_cache

Filesystem directory to use for policies caching if 'cache' is set to 'fs'.


DNSSEC

Checks whether MX records are signed. Sets MX level to "dnssec" is they are.

maddy does not validate DNSSEC signatures on its own. Instead it relies on
the upstream resolver to do so by causing lookup to fail when verification
fails and setting the AD flag for signed and verified zones. As a safety
measure, if the resolver is not 127.0.0.1 or ::1, the AD flag is ignored.

DNSSEC is currently not supported on Windows and other platforms that do not
have the /etc/resolv.conf file in the standard format.

text
dnssec { }

DANE

Checks TLSA records for the recipient MX. Provides downgrade-resistant TLS
enforcement.

Sets TLS level to "authenticated" if a valid and matching TLSA record uses
DANE-EE or DANE-TA usage type.

See above for notes on DNSSEC. DNSSEC support is required for DANE to work.

text
dane { }

Local policy

Checks effective TLS and MX levels (as set by other policies) against local
configuration.

text
local_policy {
	min_tls_level none
	min_mx_level none
}

Using local_policy off is equivalent to setting both directives to none.

min_tls_level `none` | `encrypted` | `authenticated`

Default: encrypted

Set the minimal TLS security level required for all outbound messages.

See Security levels page for details.

min_mx_level `none` | `mtasts` | `dnssec`

Default: none

Set the minimal MX security level required for all outbound messages.

See Security levels page for details.


Reference Targets Smtp (`docs/reference/targets/smtp.md`)

SMTP & LMTP transparent forwarding

Module that implements transparent forwarding of messages over SMTP.

Use in pipeline configuration:

text
deliver_to smtp tcp://127.0.0.1:5353
# or
deliver_to smtp tcp://127.0.0.1:5353 {
  # Other settings, see below.
}

target.lmtp can be used instead of target.smtp to
use LMTP protocol.

Endpoint addresses use format described in Configuration files syntax / Address definitions.

Configuration directives

text
target.smtp {
    debug no
    tls_client {
        ...
    }
    attempt_starttls yes
    require_tls no
    auth off
    targets tcp://127.0.0.1:2525
    connect_timeout 5m
    command_timeout 5m
    submission_timeout 12m
}

debug _boolean_

Default: global directive value

Enable verbose logging.


tls_client { ... }

Default: not specified

Advanced TLS client configuration options. See TLS configuration / Client for details.


starttls _boolean_

Default: yes (no for target.lmtp)

Use STARTTLS to enable TLS encryption. If STARTTLS is not supported
by the remote server - connection will fail.

maddy will use localhost as HELO hostname before STARTTLS
and will only send its actual hostname after STARTTLS.

attempt_starttls _boolean_

Default: yes (no for target.lmtp)

DEPRECATED: Equivalent to starttls. Plaintext fallback is no longer
supported.


require_tls _boolean_

Default: no

DEPRECATED: Ignored. Set starttls yes to use STARTLS.


auth `off` | `plain` _username_ _password_ | `forward` | `external`

Default: off

Specify the way to authenticate to the remote server.
Valid values:

  • off – No authentication.
  • plain – Authenticate using specified username-password pair.
    Don't use this without enforced TLS (require_tls).
  • forward – Forward credentials specified by the client.
    Don't use this without enforced TLS (require_tls).
  • external – Request "external" SASL authentication. This is usually used for
    authentication using TLS client certificates. See TLS configuration / Client for details.

targets _endpoints..._

Required.
Default: not specified

List of remote server addresses to use. See Address definitions
for syntax to use. Basically, it is tcp://ADDRESS:PORT
for plain SMTP and tls://ADDRESS:PORT for SMTPS (aka SMTP with Implicit
TLS).

Multiple addresses can be specified, they will be tried in order until connection to
one succeeds (including TLS handshake if TLS is required).


connect_timeout _duration_

Default: 5m

Same as for target.remote.


command_timeout _duration_

Default: 5m

Same as for target.remote.


submission_timeout _duration_

Default: 12m

Same as for target.remote.


--- METRICS ---