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:
/
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:
gcc pam.c main.c -lpam -o maddy-pam-helperYes, 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
chown maddy: /usr/bin/maddy-pam-helper
chmod u+x,g-x,o-x /usr/bin/maddy-pam-helperAlso maddy-pam-helper needs access to /etc/shadow, one of the ways to provide
it is to set file capability CAP_DAC_READ_SEARCH:
setcap cap_dac_read_search+ep /usr/bin/maddy-pam-helperSecond method
Another, less restrictive is to make it setuid-root (assuming you have both maddy user and group):
chown root:maddy /usr/bin/maddy-pam-helper
chmod u+xs,g+x,o-x /usr/bin/maddy-pam-helperThird 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:
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-helperPick 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
chown maddy: /usr/bin/maddy-shadow-helper
chmod u+x,g-x,o-x /usr/bin/maddy-shadow-helperAlso maddy-shadow-helper needs access to /etc/shadow, one of the ways to provide
it is to set file capability CAP_DAC_READ_SEARCH:
setcap cap_dac_read_search+ep /usr/bin/maddy-shadow-helperSecond method
Another, less restrictive is to make it setuid-root (assuming you have both maddy user and group):
chown root:maddy /usr/bin/maddy-shadow-helper
chmod u+xs,g+x,o-x /usr/bin/maddy-shadow-helperThird 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:
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-helperPick 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:
scdoc < maddy-filters.5.scd > maddy-filters.5
man ./maddy-filters.5build.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:
kubectl create secret tls maddy --cert=fullchain.pem --key=privkey.pemIf 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
helm upgrade --install maddy ./chart --set service.externapIPs[0]=1.2.3.41.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:
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 ports993,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:
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:
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
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.7It 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
I configured maddy as recommended and gmail still puts my messages in spam
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.
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 addrspamd 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:
$(primary_domain) = example.org
$(local_domains) = $(primary_domain) example.comNote 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:
auth_map email_localpartThis 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:
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:
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:
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.
$(primary_domain) = example.org
$(local_domains) = example.org example.comCreate accounts as:
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.
$(primary_domain) = example.org
$(local_domains) = example.org example.com
auth_map email_localpartCreate accounts as:
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.
$(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:
maddy creds create user
maddy imap-acct create userSeclevels (`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.
- Make sure the server is not running.
systemctl stop maddy- Take a backup of
imapsql.db*files in state directory (/var/lib/maddy).
mkdir backup
cp /var/lib/maddy/imapsql.db* backup/- Compile migration utility:
git clone https://github.com/foxcpp/maddy.git
cd maddy/
git checkout v0.3.0
cd cmd/migrate-db-0.2
go build- Run compiled binary:
./migrate-db-0.2 /var/lib/maddy/imapsql.db- Open maddy.conf and make following changes:
Remove local_authdb name from imapsql configuration block:
imapsql local_mailboxes {
driver sqlite3
dsn imapsql.db
}Add local_authdb configuration block using pass_table module:
pass_table local_authdb {
table sql_table {
driver sqlite3
dsn credentials.db
table_name passwords
}
}Use
maddy creds create ACCOUNT_NAMEto add credentials topass_table
store.Start the server back.
systemctl start maddy0.1 -> 0.2
0.2 requires several changes in configuration file.
Change
sql local_mailboxes local_authdb {to
imapsql local_mailboxes local_authdb {Replace
replace_rcpt postmaster postmaster@$(primary_domain)with
replace_rcpt static {
entry postmaster postmaster@$(primary_domain)
}and
replace_rcpt "(.+)\+(.+)@(.+)" "$1@$3"with
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
forfield is never included in theReceivedheader field.This is allowed by RFC 2821.
IMAP
`sql`
\Recentflag 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:
\Recentflag is not reset sometimes.
- Partial:
- 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:
sqlite3 -cmd 'vacuum' database_file_path_here.db
sqlite3 -cmd 'pragma wal_checkpoint(truncate)' database_file_path_here.dbIt will take some time to complete, you can close the utility when thesqlite> 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.
directive0
directive1 arg0 arg1Any 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.
directive0 two arguments
directive1 "one argument"String wrapped in quotes may contain newlines and they will not be interpreted
as a directive separator.
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:
directive0 arg0 arg1 {
subdirective0 arg0 arg1
subdirective1 etc
}Subdirectives can have blocks too.
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:
directive { }
directive2 # same as aboveEnvironment 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.
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.
(snippetname) {
a
b
c
}The snippet can then be referenced using 'import' meta-directive.
unrelated0
unrelated1
import snippetnameThe above example will be expanded into the following configuration:
unrelated0
unrelated1
a
b
cImport 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.
# /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
}# 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:
1h
1h 5m
1h5m
0Data 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:
32M
3M 5K
5bAlso note that the following is not valid, unlike Duration values syntax:
32M5KAddress 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 byauth_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:
auth_map email_localpartWith this configuration, [email protected] and [email protected] will useuser credentials when authenticating, but will access [email protected] and[email protected] mailboxes correspondingly. If you want to also acceptuser 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"):
storage_map email_localpartIn this case you will need to create storage accounts without domain part in
the name:
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:
autoprecis_casefold_emailfor valid emails,precis_casefoldotherwise.precis_casefold_emailPRECIS UsernameCaseMapped profile + U-labels form for domainprecis_casefoldPRECIS UsernameCaseMapped profile for the entire stringprecis_emailPRECIS UsernameCasePreserved profile + U-labels form for domainprecisPRECIS UsernameCasePreserved profile for the entire stringcasefoldConvert to lower casenoopNothing
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:
log syslog /var/log/maddy.logNote: 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:
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:
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:
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
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
checkblocks (if any) - Execute modifiers referenced in top-level
modifyblocks (if any) - If there are
sourceblocks - select one that matches the message sender (as
specified in MAIL FROM). If there are nosourceblocks - the entire
configuration is assumed to be thedefault_sourceblock. - Execute checks referenced in
checkblocks inside the selectedsourceblock
(if any). - Execute modifiers referenced in
modifyblocks inside selectedsource
block (if any).
Then, for each recipient:
- Select the
destinationblock that matches it. If there are
nodestinationblocks - the entire usedsourceblock is interpreted as if it
was adefault_destinationblock. - Execute checks referenced in the
checkblock inside the selecteddestination
block (if any). - Execute modifiers referenced in
modifyblock inside the selecteddestination
block (if any). - If the used block contains the
rejectdirective - reject the recipient with
the specified SMTP status code. - If the used block contains the
deliver_todirective - 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.
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 orreject directive. If destination blocks are used, thendefault_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:
destination example.org {
deliver_to local_mboxes
}
rejectIt 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:
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:
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:
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:
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 ordestination/source directives.
Example:
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:
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:
# 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:
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:
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:
destination_in sql_table {
dsn ...
driver ...
} {
deliver_to whatever
}In this case, configuration should be specified separately and be referneced
using '&' syntax:
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:
# 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:
msgpipeline local_routing {
destination whatever.com {
deliver_to dummy
}
}
# ... somewhere else ...
deliver_to &local_routingReference 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:
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:
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:
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
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.
./build.sh --tags 'libdns_googleclouddns'- gandi
dns gandi {
api_token "token"
}- digitalocean
dns digitalocean {
api_token "..."
}- cloudflare
See https://github.com/libdns/cloudflare#authenticating
dns cloudflare {
api_token "..."
}- vultr
dns vultr {
api_token "..."
}- hetzner
dns hetzner {
api_token "..."
}- namecheap
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)
dns googleclouddns {
project "project_id"
service_account_json "path"
}- route53 (non-default)
dns route53 {
secret_access_key "..."
access_key_id "..."
# or use environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
}- leaseweb (non-default)
dns leaseweb {
api_key "key"
}- metaname (non-default)
dns metaname {
api_key "key"
account_ref "reference"
}- alidns (non-default)
dns alidns {
key_id "..."
key_secret "..."
}- namedotcom (non-default)
dns namedotcom {
user "..."
token "..."
}- rfc2136 (non-default)
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)
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.
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-SHARSA-WITH-3DES-EDE-CBC-SHARSA-WITH-AES128-CBC-SHARSA-WITH-AES256-CBC-SHARSA-WITH-AES128-CBC-SHA256RSA-WITH-AES128-GCM-SHA256RSA-WITH-AES256-GCM-SHA384ECDHE-ECDSA-WITH-RC4128-SHAECDHE-ECDSA-WITH-AES128-CBC-SHAECDHE-ECDSA-WITH-AES256-CBC-SHAECDHE-RSA-WITH-RC4128-SHAECDHE-RSA-WITH-3DES-EDE-CBC-SHAECDHE-RSA-WITH-AES128-CBC-SHAECDHE-RSA-WITH-AES256-CBC-SHAECDHE-ECDSA-WITH-AES128-CBC-SHA256ECDHE-RSA-WITH-AES128-CBC-SHA256ECDHE-RSA-WITH-AES128-GCM-SHA256ECDHE-ECDSA-WITH-AES128-GCM-SHA256ECDHE-RSA-WITH-AES256-GCM-SHA384ECDHE-ECDSA-WITH-AES256-GCM-SHA384ECDHE-RSA-WITH-CHACHA20-POLY1305ECDHE-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.
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.
Get rid of
imapendpoint and existinglocal_authdbandlocal_mailboxes
blocks.Setup Dovecot to provide LMTP endpoint
Here is an example configuration snippet:
# /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:
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:
service auth {
unix_listener auth-maddy-client {
mode = 0660
user = maddy
}
}Then just configure dovecot_sasl module for submission:
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):
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:
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.
[mta]
incoming: mailman.mta.null.NullMTA
lmtp_host: 127.0.0.1
lmtp_port: 8024After that, you will need to configure maddy to send messages to Mailman.
The preferable way of doing so is destination_in and table.regexp:
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):
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:
[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: smtpsIf you do not want to use TLS and/or authentication you can create a separate
endpoint and just point Mailman to it. E.g.
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):
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:
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:
# 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:
# 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:
# 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:
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:
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
deliver_to &remote_queuewith
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:
apt-get install golang-1.26 gcc libc6-dev makeAdditionally, 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.
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
- Clone repository
$ git clone https://github.com/foxcpp/maddy.git
$ cd maddy- Select the appropriate version to build:
$ git checkout v0.8.0 # a specific release
$ git checkout master # next bugfix release
$ git checkout dev # next feature release- Build & install it
$ ./build.sh
$ sudo ./build.sh install- 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:
./build.sh --tags 'libpam'Then you should be able to replace local_authdb implementation
in default configuration with auth.pam:
auth.pam local_authdb {
use_helper no
}Helper executable
TL;DR
git clone https://github.com/foxcpp/maddy
cd maddy/cmd/maddy-pam-helper
gcc pam.c main.c -lpam -o maddy-pam-helperCopy the resulting executable into /usr/lib/maddy/ and make
it setuid-root so it can read /etc/shadow (if that's necessary):
chown root:maddy /usr/lib/maddy/maddy-pam-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helperThen you should be able to replace local_authdb implementation
in default configuration with auth.pam:
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).
#%PAM-1.0
auth required pam_unix.so
account required pam_unix.soHere is the configuration example you could use on Ubuntu
to use the authentication config system itself uses:
#%PAM-1.0
@include common-auth
@include common-account
@include common-sessionTutorials 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)
textdocker pull foxcpp/maddy:0.6See here for Docker-specific instructions.
Building from source
See here for instructions.
Arch Linux packages
For Arch Linux users,
maddyandmaddy-gitPKGBUILDs 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:
systemctl daemon-reloadAdditionally, you should create maddy user and group. Unlike most other
Linux mail servers, maddy never runs as root.
useradd -mrU -s /sbin/nologin -d /var/lib/maddy -c "maddy mail server" maddyHost 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).
$(hostname) = mx1.example.org
$(primary_domain) = example.orgIf you want to handle multiple domains, you still need to designate
one as "primary". Add all other domains to the local_domains line:
$(local_domains) = $(primary_domain) example.com other.example.comDo 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):
$ sudo setfacl -R -m u:maddy:rX /etc/ssl/mx1.example.org.crt /etc/ssl/mx1.example.org.keymaddy 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 justkillall -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:
$ 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:
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.pemFirst run
systemctl start maddyThe 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:
; 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:
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:
version: STSv1
mode: enforce
max_age: 604800
mx: mx1.example.orgNote: 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:
mx: mx1.example.org
mx: mx2.example.orgIt 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:
_25._tcp.mx1.example.org. TLSA 3 1 1 7f59d873a70e224b184c95a4eb54caa9621e47d48b4a25d312d83d96e3498238User 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:
$ 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:
$ 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.
auth.dovecot_sasl {
endpoint unix://socket_path
}
dovecot_sasl unix://socket_pathConfiguration 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.
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.
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
}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:
(&(objectClass=posixAccount)(uid={username}))Example (using ActiveDirectory):
(&(objectCategory=Person)(memberOf=CN=user-group,OU=example,DC=example,DC=org)(sAMAccountName={username})(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))Example:
(&(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 usestorage_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.
auth.netauth {
require_group "maddy-users"
debug off
}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.
go get -tags 'libpam' ...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:
- maddy is not compiled with libpam, but
maddy-pam-helperis built separately. - 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):
chown root:maddy /usr/lib/maddy/maddy-pam-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helperReference 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:
auth.pass_table [block name] {
table <table config>
}Shortened variant for inline use:
pass_table <table> [table arguments] {
[additional table config]
}Example, read username:password pair from the text file:
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.
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:
user file /etc/maddy/allowed_userspass _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.
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):
chown root:maddy /usr/lib/maddy/maddy-shadow-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-shadow-helperReference Blob Fs (`docs/reference/blob/fs.md`)
Filesystem
This module stores message bodies in a file system directory.
storage.blob.fs {
root <directory>
}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.
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:
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 keyfile_minio: use credentials for Minio present at ~/.mc/config.jsonfile_aws: use credentials for AWS S3 present at ~/.aws/credentialsiam: 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.
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
}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:
autoprecis_casefold_emailfor valid emails,precis_casefoldotherwise.precis_casefold_emailPRECIS UsernameCaseMapped profile + U-labels form for domainprecis_casefoldPRECIS UsernameCaseMapped profile for the entire stringprecis_emailPRECIS UsernameCasePreserved profile + U-labels form for domainprecisPRECIS UsernameCasePreserved profile for the entire stringcasefoldConvert to lower casenoopNothing
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.
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
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.
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.
check {
dnsbl dnsbl.example.org dnsbl2.example.org
}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_thresholdis hit, onlyreject_threshold
applies. defer_sender_rejectfrom 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
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:
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.2only → Score: 10 (matches first rule) - If DNSBL returns
127.0.0.11only → Score: 5 (matches second rule) - If DNSBL returns both
127.0.0.2and127.0.0.11→ Score: 15 (both rules match, scores sum) - If DNSBL returns both
127.0.0.2and127.0.0.3→ Score: 10 (same rule matches, counted once)
Backwards compatibility: When response blocks are not used, the legacyresponses 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:
- Changes of envelope sender address are not supported
- Removal and addition of envelope recipients is not supported
- Removal and replacement of header fields is not supported
- Headers fields can be inserted only on top
- 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.
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 thefail_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.
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:11333Configuration 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
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
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:
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:
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 bystorage_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
storage_map email_localpartNote 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 specifydelivery_map in storage module to define how to map email addresses
to storage accounts. E.g.
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:
openmetrics tcp://127.0.0.1:9749 { }Scrape endpoint would be http://127.0.0.1:9749/metrics.
Metrics
# 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).
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.
220 example.org ESMTP Service Readytls _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:
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:
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 toStateDirectory/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:
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.
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.
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.
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:
modify {
dkim example.org selector
}Configuration directives
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 ofrequire_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:
replace_rcpt <table> [table arguments] {
[extended table config]
}
replace_sender <table> [table arguments] {
[extended table config]
}Use examples:
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:
# 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:
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:
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:
JunkIn this case, message will be placed in the Junk folder.
$Label1In 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.
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):
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.
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 beforedelivery_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_emailPRECIS UsernameCaseMapped profile + U-labels form for domainprecis_casefoldPRECIS UsernameCaseMapped profile for the entire stringprecis_emailPRECIS UsernameCasePreserved profile + U-labels form for domainprecisPRECIS UsernameCasePreserved profile for the entire stringcasefoldConvert to lower casenoopNothing
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:
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:
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.
[email protected]=>test"test @ a"@example.org=>test @ a
Mappings for invalid emails are not defined (will be treated as non-existing
values).
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] anduser 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.
table.email_with_domain DOMAIN DOMAIN... { }It can be used to implement domain-level expansion for aliases if used together
with table.chain. Example:
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:
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:
file <file path>or
file {
file <file path>
}Usage example:
# Resolve SMTP address aliases using text file mapping.
modify {
replace_rcpt file /etc/maddy/aliases
}Syntax
Better demonstrated by examples:
# 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, secondvalueReference 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.
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:
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.
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:
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:
# 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:
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.
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
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
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 withTLS-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:
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.
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:
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 specifyingTLS-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.
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.
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.
dane { }Local policy
Checks effective TLS and MX levels (as set by other policies) against local
configuration.
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:
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
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 ---
- Files Extracted: 72
- Estimated Token Budget: ~49108 tokens
- Canonical Reference: https://codewiki.google/github.com/foxcpp/maddy