From afc5745722dbd63319b842c766e3243f41336c02 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Sun, 8 Jan 2023 14:04:54 +0300 Subject: [PATCH] Checkin the 0.6.2 version of the site --- css/fonts.css | 117 + css/mkdocs.css | 84 + css/style2.css | 168 + docker/index.html | 741 ++++ faq/index.html | 786 ++++ index.html | 679 ++++ internals/quirks/index.html | 685 ++++ internals/specifications/index.html | 822 +++++ internals/sqlite/index.html | 713 ++++ internals/unicode/index.html | 750 ++++ man/_generated_maddy.1/index.html | 679 ++++ man/index.html | 665 ++++ man/maddy.1.scd | 41 + man/prepare_md.py | 57 + multiple-domains/index.html | 724 ++++ reference/auth/dovecot_sasl/index.html | 684 ++++ reference/auth/external/index.html | 699 ++++ reference/auth/ldap/index.html | 748 ++++ reference/auth/pam/index.html | 697 ++++ reference/auth/pass_table/index.html | 704 ++++ reference/auth/plain_separate/index.html | 697 ++++ reference/auth/shadow/index.html | 690 ++++ reference/blob/fs/index.html | 681 ++++ reference/blob/s3/index.html | 727 ++++ reference/checks/actions/index.html | 677 ++++ reference/checks/authorize_sender/index.html | 730 ++++ reference/checks/command/index.html | 780 ++++ reference/checks/dkim/index.html | 704 ++++ reference/checks/dnsbl/index.html | 789 ++++ reference/checks/milter/index.html | 704 ++++ reference/checks/misc/index.html | 705 ++++ reference/checks/rspamd/index.html | 719 ++++ reference/checks/spf/index.html | 726 ++++ reference/config-syntax/index.html | 852 +++++ reference/endpoints/imap/index.html | 712 ++++ reference/endpoints/openmetrics/index.html | 699 ++++ reference/endpoints/smtp/index.html | 891 +++++ reference/global-config/index.html | 727 ++++ reference/modifiers/dkim/index.html | 818 +++++ reference/modifiers/envelope/index.html | 709 ++++ reference/modules/index.html | 721 ++++ reference/smtp-pipeline/index.html | 1014 +++++ reference/storage/imap-filters/index.html | 721 ++++ reference/storage/imapsql/index.html | 795 ++++ reference/table/auth/index.html | 662 ++++ reference/table/chain/index.html | 693 ++++ reference/table/email_localpart/index.html | 665 ++++ reference/table/file/index.html | 714 ++++ reference/table/regexp/index.html | 708 ++++ reference/table/sql_query/index.html | 749 ++++ reference/table/static/index.html | 679 ++++ reference/targets/queue/index.html | 730 ++++ reference/targets/remote/index.html | 855 +++++ reference/targets/smtp/index.html | 753 ++++ reference/tls-acme/index.html | 866 +++++ reference/tls/index.html | 797 ++++ search.html | 666 ++++ search/lunr.js | 3475 ++++++++++++++++++ search/main.js | 102 + search/search_index.json | 1 + search/worker.js | 133 + seclevels/index.html | 772 ++++ sitemap.xml | 308 ++ sitemap.xml.gz | Bin 0 -> 231 bytes third-party/dovecot/index.html | 748 ++++ third-party/mailman3/index.html | 742 ++++ third-party/rspamd/index.html | 690 ++++ third-party/smtp-servers/index.html | 708 ++++ tutorials/alias-to-remote/index.html | 771 ++++ tutorials/building-from-source/index.html | 719 ++++ tutorials/pam/index.html | 776 ++++ tutorials/setting-up/index.html | 910 +++++ upgrading/index.html | 778 ++++ 73 files changed, 50401 insertions(+) create mode 100644 css/fonts.css create mode 100644 css/mkdocs.css create mode 100644 css/style2.css create mode 100644 docker/index.html create mode 100644 faq/index.html create mode 100644 index.html create mode 100644 internals/quirks/index.html create mode 100644 internals/specifications/index.html create mode 100644 internals/sqlite/index.html create mode 100644 internals/unicode/index.html create mode 100644 man/_generated_maddy.1/index.html create mode 100644 man/index.html create mode 100644 man/maddy.1.scd create mode 100644 man/prepare_md.py create mode 100644 multiple-domains/index.html create mode 100644 reference/auth/dovecot_sasl/index.html create mode 100644 reference/auth/external/index.html create mode 100644 reference/auth/ldap/index.html create mode 100644 reference/auth/pam/index.html create mode 100644 reference/auth/pass_table/index.html create mode 100644 reference/auth/plain_separate/index.html create mode 100644 reference/auth/shadow/index.html create mode 100644 reference/blob/fs/index.html create mode 100644 reference/blob/s3/index.html create mode 100644 reference/checks/actions/index.html create mode 100644 reference/checks/authorize_sender/index.html create mode 100644 reference/checks/command/index.html create mode 100644 reference/checks/dkim/index.html create mode 100644 reference/checks/dnsbl/index.html create mode 100644 reference/checks/milter/index.html create mode 100644 reference/checks/misc/index.html create mode 100644 reference/checks/rspamd/index.html create mode 100644 reference/checks/spf/index.html create mode 100644 reference/config-syntax/index.html create mode 100644 reference/endpoints/imap/index.html create mode 100644 reference/endpoints/openmetrics/index.html create mode 100644 reference/endpoints/smtp/index.html create mode 100644 reference/global-config/index.html create mode 100644 reference/modifiers/dkim/index.html create mode 100644 reference/modifiers/envelope/index.html create mode 100644 reference/modules/index.html create mode 100644 reference/smtp-pipeline/index.html create mode 100644 reference/storage/imap-filters/index.html create mode 100644 reference/storage/imapsql/index.html create mode 100644 reference/table/auth/index.html create mode 100644 reference/table/chain/index.html create mode 100644 reference/table/email_localpart/index.html create mode 100644 reference/table/file/index.html create mode 100644 reference/table/regexp/index.html create mode 100644 reference/table/sql_query/index.html create mode 100644 reference/table/static/index.html create mode 100644 reference/targets/queue/index.html create mode 100644 reference/targets/remote/index.html create mode 100644 reference/targets/smtp/index.html create mode 100644 reference/tls-acme/index.html create mode 100644 reference/tls/index.html create mode 100644 search.html create mode 100644 search/lunr.js create mode 100644 search/main.js create mode 100644 search/search_index.json create mode 100644 search/worker.js create mode 100644 seclevels/index.html create mode 100644 sitemap.xml create mode 100644 sitemap.xml.gz create mode 100644 third-party/dovecot/index.html create mode 100644 third-party/mailman3/index.html create mode 100644 third-party/rspamd/index.html create mode 100644 third-party/smtp-servers/index.html create mode 100644 tutorials/alias-to-remote/index.html create mode 100644 tutorials/building-from-source/index.html create mode 100644 tutorials/pam/index.html create mode 100644 tutorials/setting-up/index.html create mode 100644 upgrading/index.html diff --git a/css/fonts.css b/css/fonts.css new file mode 100644 index 00000000..a5cecbb8 --- /dev/null +++ b/css/fonts.css @@ -0,0 +1,117 @@ +@font-face { + font-family: 'Fira Sans'; + font-style: normal; + font-weight: 400; + src: local('Fira Sans Regular'), local('FiraSans-Regular'), + url('fira-sans-v10-latin-ext_latin-regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'Fira Sans'; + font-style: italic; + font-weight: 400; + src: local('Fira Sans Italic'), local('FiraSans-Italic'), + url('fira-sans-v10-latin-ext_latin-italic.woff2') format('woff2'); +} + +@font-face { + font-family: 'Fira Sans'; + font-style: normal; + font-weight: 700; + src: local('Fira Sans Bold'), local('FiraSans-Bold'), + url('fira-sans-v10-latin-ext_latin-700.woff2') format('woff2'); +} + +@font-face { + font-family: 'Fira Sans'; + font-style: italic; + font-weight: 700; + src: local('Fira Sans Bold Italic'), local('FiraSans-BoldItalic'), + url('fira-sans-v10-latin-ext_latin-700italic.woff2') format('woff2'); +} + +@font-face { + font-family: 'Fira Mono'; + font-style: normal; + font-weight: 400; + src: local('Fira Mono Regular'), local('FiraMono-Regular'), + url('fira-mono-v8-latin-ext_latin-regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'Fira Mono'; + font-style: normal; + font-weight: 700; + src: local('Fira Mono Bold'), local('FiraMono-Bold'), + url('fira-mono-v8-latin-ext_latin-700.woff2') format('woff2'); +} + +@font-face { + font-family: 'Go'; + src: url('GoRegular.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Go'; + src: url('Go-Italic.woff') format('woff'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Go'; + src: url('Go-Bold.woff') format('woff'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Go'; + src: url('Go-BoldItalic.woff') format('woff'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'Go'; + src: url('GoMedium-Italic.woff') format('woff'); + font-weight: 500; + font-style: italic; +} + +@font-face { + font-family: 'Go'; + src: url('GoMedium.woff') format('woff'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Go Mono'; + src: url('GoMono.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Go Mono'; + src: url('GoMono-Bold.woff') format('woff'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Go Mono'; + src: url('GoMono-Italic.woff') format('woff'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Go Mono'; + src: url('GoMono-BoldItalic.woff') format('woff'); + font-weight: bold; + font-style: italic; +} diff --git a/css/mkdocs.css b/css/mkdocs.css new file mode 100644 index 00000000..ca2a828c --- /dev/null +++ b/css/mkdocs.css @@ -0,0 +1,84 @@ +/* Widen everything a little bit to make room for the side nav */ +nav, main, footer div { + max-width: 65em; +} + +@media screen and (max-width: 65em) { + nav, main { + padding-left: 0.5em; + padding-right: 0.5em; + } +} + +main { + display: grid; + grid-gap: 1rem; + grid-template-columns: 15em auto; + grid-template-areas: "sidebar content"; + align-items: stretch; +} + + +/* Move the sidebar below when the screen gets too narrow */ +@media screen and (max-width: 50em) { + main { + grid-template-columns: 100%; + grid-template-areas: "content" "sidebar"; + } +} + +div.side { + grid-area: sidebar; + /* background: var(--header-bg); */ + padding-right: 0.5em; +} + +div.side form.search { + padding: 0.7em 0 0 0.4em; +} + +div.side form.search input { + max-width: 100%; +} + +div.side ul { + list-style: none; + padding-left: 1em; +} + + +/* Element of the ToC that is currently being viewed. */ +div.toc a.current { + color: inherit; + text-decoration: none; + font-weight: bold; +} + + +/* Anchor link on the current page. */ +div.toc li.local { + font-size: smaller; +} + +div.content { + grid-area: content; +} + + +/* Adjust the default image style back to normal float. */ +div.content img { + float: none; + margin: 0 0; +} + +div.content img.logo { + float: right; +} + + +/* Non-distracting link on the titles */ +h1 a, footer a { + color: inherit; + text-decoration: none; + font-weight: inherit; +} diff --git a/css/style2.css b/css/style2.css new file mode 100644 index 00000000..596a5d2b --- /dev/null +++ b/css/style2.css @@ -0,0 +1,168 @@ +:root { + --text-color: #111; + --link-color: #4322ff; + --code-color: rgb(10, 64, 148); + /*--pre-border: rgb(152, 190, 250);*/ + --pre-border: #83bbb3; + --body-bg: #eeeeee; + --header-bg: #2d524d; + --header-fg: #ffffff; + --header-border: #e3f2fd; +} + +* { + box-sizing: border-box; +} + +body { + font-family: "Fira Sans", "Go", sans-serif; + color: var(--text-color); + background: var(--body-bg); + font-size: medium; + overflow-x: hidden; + word-break: break-word; + overflow-wrap: break-word; + margin: 0; + + /* More space between lines to make text more readable */ + line-height: 1.5; +} + +header { + padding: 0.5em 0; + background: var(--header-bg); + color: var(--header-fg); + border-bottom: solid 1px var(--header-border); + margin-bottom: 1em; +} + +nav, main { + /* Center both in page */ + margin: 0 auto; + max-width: 50em; + padding: 0; + clear: both; +} + +nav { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +nav h1 { + flex: auto; + font-size: xx-large; + margin: 0 auto; +} + +nav div.pages { + display: flex; + font-size: 0.8em; + text-align: right; + align-items: center; + margin-right: 1em; +} + +a { + color: var(--link-color); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +h1 a { + color: inherit; +} + +pre, code { + font-family: "Fira Mono", "Go Mono", monospace; +} + +pre { + padding: 1em; + color: var(--text-color); + border: 1px solid var(--pre-border); + border-left: 5px solid var(--pre-border); + white-space: pre-wrap; +} + +blockquote { + margin: 0 0 0 0; + padding-left: 1em; + background-color: #efefef; + border: 1px solid var(--pre-border); + border-left: 5px solid var(--pre-border); +} + +img { + float: right; + max-width: 40%; + max-height: 40vh; + width: auto; + height: auto; + + margin-left: 1em; + margin-right: 1em; + margin-bottom: 1em; +} + +hr { + margin-top: 2em; + margin-bottom: 2em; + margin-left: 1em; + margin-right: 1em; +} + +/* to find the second td, we select "the TDs that come after another TD", + * knowing there will be only two */ +table.kvpairs td+td { + padding-left: 2em; +} + +footer { + background: var(--header-bg); + color: var(--header-fg); + border-top: solid 1px var(--header-border); + margin-top: 1.5em; + padding: 1em 0; +} + +footer div { + text-align: right; + font-size: 90%; + + /* Center in page, same as nav and main. */ + margin: 0 auto; + max-width: 50em; + padding: 0; + + /* Small padding to the right for legibility on small screens. */ + padding-right: 0.5em; + + /* Softer text. */ + filter: opacity(50%); +} + +@media screen and (max-width: 50em) { + nav, main{ + padding-left: 0.5em; + padding-right: 0.5em; + } +} + +/* TODO +@media (prefers-color-scheme: dark) { + :root { + --text-color: rgba(255, 255, 255, 0.92); + --link-color: #aaccee; + --code-color: rgb(212, 226, 247); + --pre-border: #233140; + --body-bg: #121212; + --header-bg: #273238; + --header-border: #37474f; + } +} +*/ diff --git a/docker/index.html b/docker/index.html new file mode 100644 index 00000000..6432db6b --- /dev/null +++ b/docker/index.html @@ -0,0 +1,741 @@ + + + + + + + + +Docker - maddy + + + +
+ +
+ +
+ +
+ +

Docker

+

Official Docker image is available from Docker Hub.

+

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

+

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

+

DKIM keys are generated in /data/dkim_keys directory.

+

Image tags

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

Ports

+

All standard ports, as described in maddy docs.

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

Volumes

+

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

+

Management utility

+

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

+
docker run --rm -it -v maddydata:/data foxcpp/maddy:0.6.0 creds create foxcpp@maddy.test
+docker run --rm -it -v maddydata:/data foxcpp/maddy:0.6.0 imap-acct create foxcpp@maddy.test
+
+ +

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 foxcpp@maddy.test
+
+ +

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 587:587 \
+  -p 993:993 \
+  foxcpp/maddy:0.6
+
+ +

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/faq/index.html b/faq/index.html new file mode 100644 index 00000000..73e3f6aa --- /dev/null +++ b/faq/index.html @@ -0,0 +1,786 @@ + + + + + + + + +Frequently Asked Questions - maddy + + + +
+ +
+ +
+ +
+ +

Frequently Asked Questions

+ +

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

+

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

+

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

+

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

+

Your provider is blocking outbound SMTP traffic on port 25.

+

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

+

What is resource usage of maddy?

+

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

+

How to setup a catchall address?

+

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

+

maddy command prints a "permission denied" error

+

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

+
sudo -u maddy maddy creds ...
+
+ +

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

+

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

+

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

+

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

+

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

+

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

+

How maddy IMAP server compares to WildDuck?

+

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

+

Both support message compression.

+

Both have first-class Unicode/internationalization support.

+

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

+

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

+

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

+

How maddy SMTP server compares to ZoneMTA?

+

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

+

maddy does not require MongoDB server for deployment.

+

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

+

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

+

Is there a webmail?

+

No, at least currently.

+

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

+

Is there a content filter (spam filter)?

+

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

+

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

+

Is it production-ready?

+

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

+

Single process makes it unreliable. This is dumb!

+

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

+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 00000000..f56186fc --- /dev/null +++ b/index.html @@ -0,0 +1,679 @@ + + + + + + + + +Home - maddy + + + +
+ +
+ +
+ +
+ +
+

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.

+

builds.sr.ht status +License text +Issues tracker

+ +
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + + + diff --git a/internals/quirks/index.html b/internals/quirks/index.html new file mode 100644 index 00000000..91ff6b8b --- /dev/null +++ b/internals/quirks/index.html @@ -0,0 +1,685 @@ + + + + + + + + +Implementation quirks - maddy + + + +
+ +
+ +
+ +
+ +

Implementation quirks

+

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

+

SMTP

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

This is allowed by RFC 2821.

+

IMAP

+

sql

+
    +
  • \Recent flag is not reset in all cases.
  • +
+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/internals/specifications/index.html b/internals/specifications/index.html new file mode 100644 index 00000000..3f654183 --- /dev/null +++ b/internals/specifications/index.html @@ -0,0 +1,822 @@ + + + + + + + + +Followed specifications - maddy + + + +
+ +
+ +
+ +
+ +

Followed specifications

+

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

+

Message format

+
    +
  • RFC 2822 - Internet Message Format
  • +
  • RFC 2045 - Multipurpose Internet Mail Extensions (MIME) (part 1)
  • +
  • RFC 2046 - Multipurpose Internet Mail Extensions (MIME) (part 2)
  • +
  • RFC 2047 - Multipurpose Internet Mail Extensions (MIME) (part 3)
  • +
  • RFC 2048 - Multipurpose Internet Mail Extensions (MIME) (part 4)
  • +
  • RFC 2049 - Multipurpose Internet Mail Extensions (MIME) (part 5)
  • +
  • +

    RFC 6532 - Internationalized Email Headers

    +
  • +
  • +

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

    +
  • +
+

IMAP

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

Extensions

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

SMTP

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

Extensions

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

Misc

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

Email security

+

User authentication

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

Sender authentication

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

Recipient authentication

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

Unicode, encodings, internationalization

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

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

+

Misc

+
    +
  • RFC 5782 - DNS Blacklists and Whitelists
  • +
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/internals/sqlite/index.html b/internals/sqlite/index.html new file mode 100644 index 00000000..50b6ad3f --- /dev/null +++ b/internals/sqlite/index.html @@ -0,0 +1,713 @@ + + + + + + + + +maddy & SQLite - maddy + + + +
+ +
+ +
+ +
+ +

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.db
+
+ +

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/internals/unicode/index.html b/internals/unicode/index.html new file mode 100644 index 00000000..89820a22 --- /dev/null +++ b/internals/unicode/index.html @@ -0,0 +1,750 @@ + + + + + + + + +Unicode support - maddy + + + +
+ +
+ +
+ +
+ +

Unicode support

+

maddy has the first-class Unicode support in all components (modules). You do +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 messsages and process them locally.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/man/_generated_maddy.1/index.html b/man/_generated_maddy.1/index.html new file mode 100644 index 00000000..880aefba --- /dev/null +++ b/man/_generated_maddy.1/index.html @@ -0,0 +1,679 @@ + + + + + + + + +Command line arguments - maddy + + + +
+ +
+ +
+ +
+ +

Command line arguments

+

Name

+

maddy - Composable all-in-one mail server.

+

Synopsis

+

maddy [options...]

+

Description

+

Maddy is Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission +Agent (MSA), IMAP server and a set of other essential protocols/schemes +necessary to run secure email server implemented in one executable.

+

Command line arguments

+

-h, -help + Show help message and exit.

+

-config _path_ + Path to the configuration file. Default is /etc/maddy/maddy.conf.

+

-libexec _path_ + Path to the libexec directory. Helper executables will be searched here. + Default is /usr/lib/maddy.

+

-log _targets..._ + Comma-separated list of logging targets. Valid values are the same as the + 'log' config directive. Affects logging before configuration parsing + completes and after it, if the different value is not specified in the + configuration.

+

-debug + Enable debug log. You want to use it when reporting bugs.

+

-v + Print version & build metadata.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/man/index.html b/man/index.html new file mode 100644 index 00000000..36127e3e --- /dev/null +++ b/man/index.html @@ -0,0 +1,665 @@ + + + + + + + + +Index - maddy + + + +
+ +
+ +
+ +
+ +

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.5
+
+ +

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/man/maddy.1.scd b/man/maddy.1.scd new file mode 100644 index 00000000..9ae63d5f --- /dev/null +++ b/man/maddy.1.scd @@ -0,0 +1,41 @@ +maddy(1) "maddy mail server" "maddy reference documentation" + +; TITLE Command line arguments + +# Name + +maddy - Composable all-in-one mail server. + +# Synopsis + +*maddy* [options...] + +# Description + +Maddy is Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission +Agent (MSA), IMAP server and a set of other essential protocols/schemes +necessary to run secure email server implemented in one executable. + +# Command line arguments + +*-h, -help* + Show help message and exit. + +*-config* _path_ + Path to the configuration file. Default is /etc/maddy/maddy.conf. + +*-libexec* _path_ + Path to the libexec directory. Helper executables will be searched here. + Default is /usr/lib/maddy. + +*-log* _targets..._ + Comma-separated list of logging targets. Valid values are the same as the + 'log' config directive. Affects logging before configuration parsing + completes and after it, if the different value is not specified in the + configuration. + +*-debug* + Enable debug log. You want to use it when reporting bugs. + +*-v* + Print version & build metadata. diff --git a/man/prepare_md.py b/man/prepare_md.py new file mode 100644 index 00000000..9aeb335b --- /dev/null +++ b/man/prepare_md.py @@ -0,0 +1,57 @@ +#!/usr/bin/python3 + +""" +This script does all necessary pre-processing to convert scdoc format into +Markdown. + +Usage: + prepare_md.py < in > out + prepare_md.py file1 file2 file3 + Converts into _generated_file1.md, etc. +""" + +import sys +import re + +anchor_escape = str.maketrans(r' #()./\+-_', '__________') + +def prepare(r, w): + new_lines = list() + title = str() + previous_h1_anchor = '' + + inside_literal = False + + for line in r: + if not inside_literal: + if line.startswith('; TITLE ') and title == '': + title = line[8:] + if line[0] == ';': + continue + # turn *page*(1) into [**page(1)**](../_generated_page.1) + line = re.sub(r'\*(.+?)\*\(([0-9])\)', r'[*\1(\2)*](../_generated_\1.\2)', line) + # *aaa* => **aaa** + line = re.sub(r'\*(.+?)\*', r'**\1**', line) + # remove ++ from line endings + line = re.sub(r'\+\+$', '
', line) + # turn whatever looks like a link into one + line = re.sub(r'(https://[^ \)\(\\]+[a-z0-9_\-])', r'[\1](\1)', line) + # escape underscores inside words + line = re.sub(r'([^ ])_([^ ])', r'\1\\_\2', line) + + if line.startswith('```'): + inside_literal = not inside_literal + + new_lines.append(line) + + if title != '': + print('#', title, file=w) + + print(''.join(new_lines[1:]), file=w) + +if len(sys.argv) == 1: + prepare(sys.stdin, sys.stdout) +else: + for f in sys.argv[1:]: + new_name = '_generated_' + f[:-4] + '.md' + prepare(open(f, 'r'), open(new_name, 'w')) diff --git a/multiple-domains/index.html b/multiple-domains/index.html new file mode 100644 index 00000000..61e96d76 --- /dev/null +++ b/multiple-domains/index.html @@ -0,0 +1,724 @@ + + + + + + + + +Multiple domains configuration - maddy + + + +
+ +
+ +
+ +
+ +

Multiple domains configuration

+

Separate account namespaces

+

Given two domains, example.org and example.com. foo@example.org and +foo@example.com are different and completely independent accounts.

+

All changes needed to make it work is to make sure all domains are specified in +the $(local_domains) macro in the main configuration file. Note that you need +to pick one domain as a "primary" for use in auto-generated messages.

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

The base configuration is 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.

+

Single account namespace

+

You can configure maddy to only use local part of the email +as an account identifier instead of the complete email.

+

This needs two changes to default configuration:

+
storage.imapsql local_mailboxes {
+    ...
+    delivery_map email_localpart
+    auth_normalize precis_casefold
+}
+
+ +

This way, when authenticating as foxcpp, it will be mapped to +foxcpp storage account. E.g. you will need to run +maddy imap-accts create foxcpp, without the domain part.

+

If you have existing accounts, you will need to rename them.

+

Change to auth_normalize is necessary so that normalization function +will not attempt to parse authentication identity as a email.

+

When a email is received, delivery_map email_localpart will strip +the domain part before looking up the account. That is, +foxcpp@example.org will be become just foxcpp.

+

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

+
authorize_sender {
+  ...
+  auth_normalize precis_casefold
+  user_to_email regexp "(.*)" "$1@$(primary_domain)"
+}
+
+ +

Note that is would work only if clients use only one domain as sender ($(primary_domain)). +If you want to allow sending from all domains, you need to remove authorize_sender check +altogether since it is not currently supported.

+

After that you can create accounts without specifying the domain part:

+
maddy imap-acct create foxcpp
+maddy creds create foxcpp
+
+ +

And authenticate using "foxcpp" in email clients.

+

Messages for any foxcpp@* address with a domain in $(local_domains) +will be delivered to that mailbox.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/auth/dovecot_sasl/index.html b/reference/auth/dovecot_sasl/index.html new file mode 100644 index 00000000..5ca1390a --- /dev/null +++ b/reference/auth/dovecot_sasl/index.html @@ -0,0 +1,684 @@ + + + + + + + + +Dovecot SASL - maddy + + + +
+ +
+ +
+ +
+ +

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_path
+
+ +

Configuration directives

+

Syntax: 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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/auth/external/index.html b/reference/auth/external/index.html new file mode 100644 index 00000000..711b7e73 --- /dev/null +++ b/reference/auth/external/index.html @@ -0,0 +1,699 @@ + + + + + + + + +System command - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: helper _file_path_

+

Location of the helper binary. Required.

+

Syntax: 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.

+

Syntax: 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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/auth/ldap/index.html b/reference/auth/ldap/index.html new file mode 100644 index 00000000..48ea62bc --- /dev/null +++ b/reference/auth/ldap/index.html @@ -0,0 +1,748 @@ + + + + + + + + +LDAP BindDN - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: 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.

+

Syntax: bind off
+bind unauth
+bind external
+bind 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.

+

Syntax: dn_template _template_

+

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

+

Syntax: base_dn _dn_

+

Base DN to use for lookup.

+

Syntax: 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}))
+
+ +

Syntax: starttls bool
+Default: off

+

Whether to upgrade connection to TLS using STARTTLS.

+

Syntax: tls_client { ... }

+

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

+

Syntax: connect_timeout duration
+Default: 1m

+

Timeout for initial connection to the directory server.

+

Syntax: request_timeout duration
+Default: 1m

+

Timeout for each request (binding, lookup).

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/auth/pam/index.html b/reference/auth/pam/index.html new file mode 100644 index 00000000..d0be0314 --- /dev/null +++ b/reference/auth/pam/index.html @@ -0,0 +1,697 @@ + + + + + + + + +PAM - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: debug boolean
+Default: no

+

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

+

Syntax: use_helper boolean
+Default: no

+

Use LibexecDirectory/maddy-pam-helper instead of directly calling libpam. +You need to use that if: +1. maddy is not compiled with libpam, but maddy-pam-helper is built separately. +2. maddy is running as an unprivileged user and used PAM configuration requires additional + privileges (e.g. when using system accounts).

+

For 2, you need to make maddy-pam-helper binary setuid, see +README.md in source tree for details.

+

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-helper
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/auth/pass_table/index.html b/reference/auth/pass_table/index.html new file mode 100644 index 00000000..293e8fec --- /dev/null +++ b/reference/auth/pass_table/index.html @@ -0,0 +1,704 @@ + + + + + + + + +Password table - maddy + + + +
+ +
+ +
+ +
+ +

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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/auth/plain_separate/index.html b/reference/auth/plain_separate/index.html new file mode 100644 index 00000000..cac09a75 --- /dev/null +++ b/reference/auth/plain_separate/index.html @@ -0,0 +1,697 @@ + + + + + + + + +Separate username and password lookup - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: user _table module_

+

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

+

Example:

+
user file /etc/maddy/allowed_users
+
+ +

Syntax: pass _auth provider_

+

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

+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/auth/shadow/index.html b/reference/auth/shadow/index.html new file mode 100644 index 00000000..19d08db3 --- /dev/null +++ b/reference/auth/shadow/index.html @@ -0,0 +1,690 @@ + + + + + + + + +/etc/shadow - maddy + + + +
+ +
+ +
+ +
+ +

/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

+

Syntax: debug boolean
+Default: no

+

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

+

Syntax: 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-helper
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/blob/fs/index.html b/reference/blob/fs/index.html new file mode 100644 index 00000000..c2f2e1ad --- /dev/null +++ b/reference/blob/fs/index.html @@ -0,0 +1,681 @@ + + + + + + + + +Filesystem - maddy + + + +
+ +
+ +
+ +
+ +

Filesystem

+

This module stores message bodies in a file system directory.

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

Configuration directives

+

Syntax: 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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/blob/s3/index.html b/reference/blob/s3/index.html new file mode 100644 index 00000000..0868ec5f --- /dev/null +++ b/reference/blob/s3/index.html @@ -0,0 +1,727 @@ + + + + + + + + +Amazon S3 - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: endpoint _address:port_

+

REQUIRED.

+

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

+

Syntax: secure boolean
+Default: yes

+

Whether TLS should be used.

+

Syntax: access_key string
+Syntax: secret_key _string_

+

REQUIRED.

+

Static S3 credentials.

+

Syntax: bucket _name_

+

REQUIRED.

+

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

+

Syntax: region string
+Default: not set

+

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

+

Syntax: 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.

+

Syntax: creds string
+Default: access_key

+

Credentials to use for accessing the S3 Bucket.

+

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

+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/actions/index.html b/reference/checks/actions/index.html new file mode 100644 index 00000000..dd7d6d81 --- /dev/null +++ b/reference/checks/actions/index.html @@ -0,0 +1,677 @@ + + + + + + + + +Check actions - maddy + + + +
+ +
+ +
+ +
+ +

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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/authorize_sender/index.html b/reference/checks/authorize_sender/index.html new file mode 100644 index 00000000..8afe7a94 --- /dev/null +++ b/reference/checks/authorize_sender/index.html @@ -0,0 +1,730 @@ + + + + + + + + +MAIL FROM and From authorization - maddy + + + +
+ +
+ +
+ +
+ +

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 precis_casefold_email
+    from_normalize precis_casefold_email
+}
+
+ +
check {
+    authorize_sender { ... }
+}
+
+ +

Configuration directives

+

Syntax: user_to_email table
+Default: identity

+

Table to use for lookups. Result of the lookup should contain either the +domain name, the full email address or "" string. If it is just domain - user +will be allowed to use any mailbox within a domain as a sender address. +If result contains "" - user will be allowed to use any address.

+

Syntax: 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.

+

Syntax: unauth_action action
+Default: reject

+

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

+

Syntax: no_match_action action
+Default: reject

+

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

+

Syntax: malformed_action action
+Default: reject

+

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

+

Syntax: err_action action
+Default: reject

+

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

+

Syntax: auth_normalize action
+Default: precis_casefold_email

+

Normalization function to apply to authorization username before +further processing.

+

Available options: +- precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain +- precis_casefold PRECIS UsernameCaseMapped profile for the entire string +- precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain +- precis PRECIS UsernameCasePreserved profile for the entire string +- casefold Convert to lower case +- noop Nothing

+

Syntax: from_normalize action
+Default: precis_casefold_email

+

Normalization function to apply to email addresses before +further processing.

+

Available options are same as for auth_normalize.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/command/index.html b/reference/checks/command/index.html new file mode 100644 index 00000000..d4e0ed60 --- /dev/null +++ b/reference/checks/command/index.html @@ -0,0 +1,780 @@ + + + + + + + + +System command filter - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: 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}.

+

Syntax:
+code integer ignore
+code integer quarantine
+code integer reject [SMTP code] [SMTP enhanced code] [SMTP message]

+

This directives specified 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 +action can be overriden using the 'code' directive.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/dkim/index.html b/reference/checks/dkim/index.html new file mode 100644 index 00000000..a943902c --- /dev/null +++ b/reference/checks/dkim/index.html @@ -0,0 +1,704 @@ + + + + + + + + +DKIM - maddy + + + +
+ +
+ +
+ +
+ +

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
+}
+
+ +

Syntax: debug boolean
+Default: global directive value

+

Log both successfull and unsuccessful check executions instead of just +unsuccessful.

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: 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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/dnsbl/index.html b/reference/checks/dnsbl/index.html new file mode 100644 index 00000000..1e5bff4e --- /dev/null +++ b/reference/checks/dnsbl/index.html @@ -0,0 +1,789 @@ + + + + + + + + +DNSBL lookup - maddy + + + +
+ +
+ +
+ +
+ +

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 specifing 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
+    }
+}
+
+ +

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

+

Syntax: debug boolean
+Default: global directive value

+

Enable verbose logging.

+

Syntax: check_early boolean
+Default: no

+

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

+

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

+

In particular, this means: +- No logging is done for rejected messages. +- No action is taken if quarantine_threshold is hit, only reject_threshold + applies. +- defer_sender_reject from SMTP configuration takes no effect. +- MAIL FROM is not checked, even if specified.

+

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

+

Syntax: quarantine_threshold integer
+Default: 1

+

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

+

Syntax: 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.

+

Syntax: client_ipv4 boolean
+Default: yes

+

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

+

Syntax: client_ipv6 boolean
+Default: yes

+

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

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: 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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/milter/index.html b/reference/checks/milter/index.html new file mode 100644 index 00000000..bb9068a7 --- /dev/null +++ b/reference/checks/milter/index.html @@ -0,0 +1,704 @@ + + + + + + + + +Milter client - maddy + + + +
+ +
+ +
+ +
+ +

Milter client

+

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

+

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

+

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

+
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

+

Syntax: 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'

+

Syntax: 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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/misc/index.html b/reference/checks/misc/index.html new file mode 100644 index 00000000..513ec874 --- /dev/null +++ b/reference/checks/misc/index.html @@ -0,0 +1,705 @@ + + + + + + + + +Misc checks - maddy + + + +
+ +
+ +
+ +
+ +

Misc checks

+

Configuration directives

+

Following directives are defined for all modules listed below.

+

Syntax:
+fail_action ignore
+fail_action reject
+fail_action quarantine
+Default: quarantine

+

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

+

Syntax: debug boolean
+Default: global directive value

+

Log both sucessfull and unsucessfull check executions instead of just +unsucessfull.

+

require_mx_record

+

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

+

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

+

require_matching_rdns

+

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

+

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

+

require_tls

+

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

+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/rspamd/index.html b/reference/checks/rspamd/index.html new file mode 100644 index 00000000..b7e106d4 --- /dev/null +++ b/reference/checks/rspamd/index.html @@ -0,0 +1,719 @@ + + + + + + + + +rspamd - maddy + + + +
+ +
+ +
+ +
+ +

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
+    flags pass_all
+}
+
+rspamd http://127.0.0.1:11333
+
+ +

Configuration directives

+

Syntax: tls_client { ... }
+Default: not set

+

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

+

Syntax: 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.

+

Syntax: settings_id string
+Default: not set

+

Settings ID to pass to the server.

+

Syntax: tag string
+Default: maddy

+

Value to send in MTA-Tag header field.

+

Syntax: hostname string
+Default: value of global directive

+

Value to send in MTA-Name header field.

+

Syntax: io_error_action action
+Default: ignore

+

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

+

Syntax: error_resp_action action
+Default: ignore

+

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

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: flags string list...
+Default: pass_all

+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/checks/spf/index.html b/reference/checks/spf/index.html new file mode 100644 index 00000000..394e9d23 --- /dev/null +++ b/reference/checks/spf/index.html @@ -0,0 +1,726 @@ + + + + + + + + +SPF - maddy + + + +
+ +
+ +
+ +
+ +

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
+}
+
+ +

Syntax: debug boolean
+Default: global directive value

+

Enable verbose logging for check.spf.

+

Syntax: 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).

+

Syntax: none_action reject|qurantine|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.

+

Syntax: neutral_action reject|qurantine|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.

+

Syntax: fail_action reject|qurantine|ignore
+Default: quarantine

+

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

+

Syntax: softfail_action reject|qurantine|ignore
+Default: ignore

+

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

+

Syntax: permerr_action reject|qurantine|ignore
+Default: reject

+

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

+

Syntax: temperr_action reject|qurantine|ignore
+Default: reject

+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/config-syntax/index.html b/reference/config-syntax/index.html new file mode 100644 index 00000000..ad9afbb7 --- /dev/null +++ b/reference/config-syntax/index.html @@ -0,0 +1,852 @@ + + + + + + + + +Configuration files syntax - maddy + + + +
+ +
+ +
+ +
+ +

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 arg1
+
+ +

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

+

Quoting

+

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

+
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 above
+
+ +

Environment variables

+

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

+

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

+
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 snippetname
+
+ +

The above example will be expanded into the following configuration:

+
unrelated0
+unrelated1
+a
+b
+c
+
+ +

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

+
# /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
+0
+
+ +

Data size values

+

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

+

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

+

Examples:

+
32M
+3M 5K
+5b
+
+ +

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

+
32M5K
+
+ +

ADDRESS DEFINITIONS

+

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

+
    +
  • +

    unix://file_path + Unix domain socket. Relative paths are relative to runtime directory + (/run/maddy).

    +
  • +
  • +

    tcp://ADDRESS:PORT + TCP/IP socket.

    +
  • +
  • +

    tls://ADDRESS:PORT + TCP/IP socket using TLS.

    +
  • +
+

DUMMY MODULE

+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/endpoints/imap/index.html b/reference/endpoints/imap/index.html new file mode 100644 index 00000000..0800f0fb --- /dev/null +++ b/reference/endpoints/imap/index.html @@ -0,0 +1,712 @@ + + + + + + + + +IMAP4rev1 endpoint - maddy + + + +
+ +
+ +
+ +
+ +

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
+    auth pam
+    storage &local_mailboxes
+}
+
+ +

Syntax: tls certificate_path key_path { ... }
+Default: global directive value

+

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

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

See TLS configuration / Server for details.

+

Syntax: io_debug boolean
+Default: no

+

Write all commands and responses to stderr.

+

Syntax: io_errors boolean
+Default: no

+

Log I/O errors.

+

Syntax: debug boolean
+Default: global directive value

+

Enable verbose logging.

+

Syntax: insecure_auth boolean
+Default: no (yes if TLS is disabled)

+

Syntax: auth _module_reference_

+

Use the specified module for authentication. +Required.

+

Syntax: storage _module_reference_

+

Use the specified module for message storage. +Required.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/endpoints/openmetrics/index.html b/reference/endpoints/openmetrics/index.html new file mode 100644 index 00000000..ea26ccf7 --- /dev/null +++ b/reference/endpoints/openmetrics/index.html @@ -0,0 +1,699 @@ + + + + + + + + +OpenMetrics/Prometheus telemetry - maddy + + + +
+ +
+ +
+ +
+ +

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}
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/endpoints/smtp/index.html b/reference/endpoints/smtp/index.html new file mode 100644 index 00000000..167befa8 --- /dev/null +++ b/reference/endpoints/smtp/index.html @@ -0,0 +1,891 @@ + + + + + + + + +SMTP/LMTP/Submission endpoint - maddy + + + +
+ +
+ +
+ +
+ +

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
+    read_timeout 10m
+    write_timeout 1m
+    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 ocnfiguration.
+    destination example.org {
+        deliver_to &local_mailboxes
+    }
+    default_destination {
+        reject
+    }
+}
+
+ +

Configuration directives

+

Syntax: hostname string
+Default: global directive value

+

Server name to use in SMTP banner.

+
220 example.org ESMTP Service Ready
+
+ +

Syntax: tls certificate_path key_path { ... }
+Default: global directive value

+

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

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

See TLS configuration / Server for details.

+

Syntax: io_debug boolean
+Default: no

+

Write all commands and responses to stderr.

+

Syntax: debug boolean
+Default: global directive value

+

Enable verbose logging.

+

Syntax: insecure_auth boolean
+Default: no (yes if TLS is disabled)

+

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

+

Syntax: read_timeout duration
+Default: 10m

+

I/O read timeout.

+

Syntax: write_timeout duration
+Default: 1m

+

I/O write timeout.

+

Syntax: max_message_size size
+Default: 32M

+

Limit the size of incoming messages to 'size'.

+

Syntax: max_header_size size
+Default: 1M

+

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

+

Syntax: auth module_reference
+Default: not specified

+

Use the specified module for authentication.

+

Syntax: 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.

+

Syntax: 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 dictonary +attacks (address probing).

+

Syntax: 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").

+

Syntax:
+buffer ram
+buffer fs [path]
+buffer auto max_size [path]
+Default: auto 1M StateDirectory/buffer

+

Temporary storage to use for the body of accepted messages.

+
    +
  • ram
  • +
+

Store the body in RAM.

+
    +
  • fs
  • +
+

Write out the message to the FS and read it back as needed. +path can be omitted and defaults to StateDirectory/buffer.

+
    +
  • auto
  • +
+

Store message bodies smaller than max_size entirely in RAM, otherwise write +them out to the FS. +path can be omitted and defaults to StateDirectory/buffer.

+

Syntax: 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.

+

Syntax: 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

+

Syntax: limits config block
+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:

+
    +
  • Rate limit
  • +
+

Syntax: scope rate burst [period]
+Restrict the amount of messages processed in period to burst messages. +If period is not specified, 1 second is used.

+
    +
  • Concurrency limit
  • +
+

Syntax: scope concurrency max
+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 in a scond. "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, additionaly 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.

    +
  • +
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/global-config/index.html b/reference/global-config/index.html new file mode 100644 index 00000000..066bfdab --- /dev/null +++ b/reference/global-config/index.html @@ -0,0 +1,727 @@ + + + + + + + + +Global configuration directives - maddy + + + +
+ +
+ +
+ +
+ +

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).

+

Syntax: 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.

+

Syntax: runtime_dir path
+Default: /run/maddy

+

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

+

Syntax: 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.

+

Syntax: autogenerated_msg_domain domain
+Default: not specified

+

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

+

Syntax:
+tls file cert_file pkey_file
+tls module reference
+tls 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.

+

Syntax: 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.

+

Syntax:
+log targets...
+log 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.log
+
+ +

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

+

Syntax: debug boolean
+Default: no

+

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

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/modifiers/dkim/index.html b/reference/modifiers/dkim/index.html new file mode 100644 index 00000000..55b4d998 --- /dev/null +++ b/reference/modifiers/dkim/index.html @@ -0,0 +1,818 @@ + + + + + + + + +DKIM signing - maddy + + + +
+ +
+ +
+ +
+ +

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
+}
+
+ +

Syntax: debug boolean
+Default: global directive value

+

Enable verbose logging.

+

Syntax: domains string list
+Default: not specified

+

REQUIRED.

+

ADministrative Management Domains (ADMDs) taking responsibility for messages.

+

Should be specified either as a directive or as an argument.

+

Syntax: selector string
+Default: not specified

+

REQUIRED.

+

Identifier of used key within the ADMD. +Should be specified either as a directive or as an argument.

+

Syntax: 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.

+

Syntax: 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

+

Syntax: sign_fields list...
+Default: see below

+

Header fields that should be signed n+1 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

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: sig_expiry duration
+Default: 120h

+

Time for which signature should be considered valid. Mainly used to prevent +unauthorized resending of old messages.

+

Syntax: hash hash
+Default: sha256

+

Hash algorithm to use when computing body hash.

+

sha256 is the only supported algorithm now.

+

Syntax: newkey_algo rsa4096|rsa2048|ed25519
+Default: rsa2048

+

Algorithm to use when generating a new key.

+

Currently ed25519 is NOT supported by most platforms.

+

Syntax: 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).

+

Syntax: allow_multiple_from boolean
+Default: no

+

Allow multiple addresses in From header field for purposes of +require_sender_match checks. Only first address will be checked, however.

+

Syntax: sign_subdomains boolean
+Default: no

+

Sign emails from subdomains using a top domain key.

+

Allows only one domain to be specified (can be workarounded using modify.dkim +multiple times).

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/modifiers/envelope/index.html b/reference/modifiers/envelope/index.html new file mode 100644 index 00000000..d7165739 --- /dev/null +++ b/reference/modifiers/envelope/index.html @@ -0,0 +1,709 @@ + + + + + + + + +Envelope sender / recipient rewriting - maddy + + + +
+ +
+ +
+ +
+ +

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 a@example.org b@example.org
+        entry c@example.org c1@example.org c2@example.org
+    }
+    replace_rcpt regexp "(.+)@example.net" "$1@example.org"
+    replace_rcpt regexp "(.+)@example.net" "$1@example.org" "$1@example.com"
+}
+
+ +

Possible contents of /etc/maddy/aliases in the example above:

+
# Replace 'cat' with any domain to 'dog'.
+# E.g. cat@example.net -> dog@example.net
+cat: dog
+
+# Replace cat@example.org with cat@example.com.
+# Takes priority over the previous line.
+cat@example.org: cat@example.com
+
+# Using aliases in multiple lines
+cat2: dog
+cat2: mouse
+cat2@example.org: cat@example.com
+cat2@example.org: cat@example.net
+# Comma-separated aliases in multiple lines
+cat3: dog , mouse
+cat3@example.org: cat@example.com , cat@example.net
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/modules/index.html b/reference/modules/index.html new file mode 100644 index 00000000..3313a159 --- /dev/null +++ b/reference/modules/index.html @@ -0,0 +1,721 @@ + + + + + + + + +Modules introduction - maddy + + + +
+ +
+ +
+ +
+ +

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
+}
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/smtp-pipeline/index.html b/reference/smtp-pipeline/index.html new file mode 100644 index 00000000..4a9ae269 --- /dev/null +++ b/reference/smtp-pipeline/index.html @@ -0,0 +1,1014 @@ + + + + + + + + +SMTP message routing (pipeline) - maddy + + + +
+ +
+ +
+ +
+ +

SMTP message routing (pipeline)

+

Message pipeline

+

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).

    +
  • +
  • +

    Assocating each message recipient with one or more delivery targets. + Delivery target is a module that does final processing (delivery) of the + message.

    +
  • +
+

Message handling flow is as follows: +- Execute checks referenced in top-level 'check' blocks (if any)

+
    +
  • +

    Execute modifiers referenced in top-level 'modify' blocks (if any)

    +
  • +
  • +

    If there are 'source' blocks - select one that matches message sender (as + specified in MAIL FROM). If there are no 'source' blocks - entire + configuration is assumed to be the 'default_source' block.

    +
  • +
  • +

    Execute checks referenced in 'check' blocks inside selected 'source' block + (if any).

    +
  • +
  • +

    Execute modifiers referenced in 'modify' blocks inside selected 'source' + block (if any).

    +
  • +
+

Then, for each recipient: +- Select 'destination' block that matches it. If there are + no 'destination' blocks - entire used 'source' block is interpreted as if it + was a 'default_destination' block.

+
    +
  • +

    Execute checks referenced in 'check' block inside selected 'destination' block + (if any).

    +
  • +
  • +

    Execute modifiers referenced in 'modify' block inside selected 'destination' + block (if any).

    +
  • +
  • +

    If used block contains 'reject' directive - reject the recipient with + specified SMTP status code.

    +
  • +
  • +

    If used block contains 'deliver_to' directive - pass the message to the + specified target module. Only recipients that are handled + by used block are visible to the target.

    +
  • +
+

Each recipient is handled only by a single 'destination' block, in case of +overlapping 'destination' - 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 or +'reject' directive. If 'destination' blocks are used, then +'default_destination' block should also be used to specify behavior for +unmatched recipients. Same goes for source blocks, 'default_source' should be +used if 'source' is used.

+

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

+

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

+
destination example.org {
+    deliver_to local_mboxes
+}
+reject
+
+ +

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

+

Complete configuration example using all of the mentioned directives:

+
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

+

Syntax: 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
+}
+
+ +

Syntax: 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
+}
+
+ +

Syntax:
+reject smtp_code smtp_enhanced_code error_description
+reject smtp_code smtp_enhanced_code
+reject smtp_code
+reject
+Context: destination block

+

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

+

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

+

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

+

Example:

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

Syntax: 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.

+

Syntax: 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.

+

Syntax: 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"
+}
+
+ +

Syntax: 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.

+

Syntax: 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
+    }
+}
+
+ +

Syntax: 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_routing
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/storage/imap-filters/index.html b/reference/storage/imap-filters/index.html new file mode 100644 index 00000000..bffa5ab0 --- /dev/null +++ b/reference/storage/imap-filters/index.html @@ -0,0 +1,721 @@ + + + + + + + + +IMAP filters - maddy + + + +
+ +
+ +
+ +
+ +

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 +eariler 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 +acccess 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:

+
Junk
+
+ +

In this case, message will be placed in the Junk folder.

+
$Label1
+
+ +

In this case, message will be placed in inbox and will have +'$Label1' added.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/storage/imapsql/index.html b/reference/storage/imapsql/index.html new file mode 100644 index 00000000..576dcfd2 --- /dev/null +++ b/reference/storage/imapsql/index.html @@ -0,0 +1,795 @@ + + + + + + + + +SQL-indexed storage - maddy + + + +
+ +
+ +
+ +
+ +

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 ".*" "catchall@example.org"
+    }
+    deliver_to &local_mailboxes
+}
+
+ +

Arguments

+

Specify the driver and DSN.

+

Configuration directives

+

Syntax: driver string
+Default: not specified

+

REQUIRED.

+

Use a specified driver to communicate with the database. Supported values: +sqlite3, postgres.

+

Should be specified either via an argument or via this directive.

+

Syntax: dsn string
+Default: not specified

+

REQUIRED.

+

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.

+

Syntax: msg_store store
+Default: fs messages/

+

Module to use for message bodies storage.

+

See "Blob storage" section for what you can use here.

+

Syntax:
+compression off
+compression algorithm
+compression algorithm level
+Default: off

+

Apply compression to message contents. +Supported algorithms: lz4, zstd.

+

Syntax: 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.

+

Syntax: debug boolean
+Default: global directive value

+

Enable verbose logging.

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: sqlite_busy_timeout integer
+Default: 5000000

+

SQLite-specific performance tuning option. Amount of milliseconds to wait +before giving up on DB lock.

+

Syntax: 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}
+}
+
+ +

Syntax: delivery_map table
+Default: identity

+

Use specified table module to map recipient +addresses from incoming messages to mailbox names.

+

Normalization algorithm specified in delivery_normalize is appied before +delivery_map.

+

Syntax: delivery_normalize name
+Default: precis_casefold_email

+

Normalization function to apply to email addresses before mapping them +to mailboxes.

+

See auth_normalize.

+

Syntax: auth_map table
+Default: identity

+

Use specified table module to map authentication +usernames to mailbox names.

+

Normalization algorithm specified in auth_normalize is applied before +auth_map.

+

Syntax: auth_normalize name
+Default: precis_casefold_email

+

Normalization function to apply to authentication usernames before mapping +them to mailboxes.

+

Available options: +- precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain +- precis_casefold PRECIS UsernameCaseMapped profile for the entire string +- precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain +- precis PRECIS UsernameCasePreserved profile for the entire string +- casefold Convert to lower case +- noop Nothing

+

Note: On message delivery, recipient address is unconditionally normalized +using precis_casefold_email function.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/table/auth/index.html b/reference/table/auth/index.html new file mode 100644 index 00000000..d4b9b176 --- /dev/null +++ b/reference/table/auth/index.html @@ -0,0 +1,662 @@ + + + + + + + + +Authentication providers - maddy + + + +
+ +
+ +
+ +
+ +

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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/table/chain/index.html b/reference/table/chain/index.html new file mode 100644 index 00000000..7b967432 --- /dev/null +++ b/reference/table/chain/index.html @@ -0,0 +1,693 @@ + + + + + + + + +Table chaining - maddy + + + +
+ +
+ +
+ +
+ +

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 "(.+)(\\+[^+"@]+)?@example.org" "$1@example.org"
+    step file /etc/maddy/emails
+}
+
+ +

This will strip +prefix from mailbox before looking it up +in /etc/maddy/emails list.

+

Configuration directives

+

Syntax: step _table_

+

Adds a table module to the chain. If input value is not in the table +(e.g. file) - return "not exists" error.

+

Syntax: 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 regexp "(.+)@(.+)" "$1"
+}
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/table/email_localpart/index.html b/reference/table/email_localpart/index.html new file mode 100644 index 00000000..fd4c75f2 --- /dev/null +++ b/reference/table/email_localpart/index.html @@ -0,0 +1,665 @@ + + + + + + + + +Email local part - maddy + + + +
+ +
+ +
+ +
+ +

Email local part

+

The module 'table.email_localpart' extracts and unescaped local ("username") part +of the email address.

+

E.g. +test@example.org => test +"test @ a"@example.org => test @ a

+
table.email_localpart { }
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/table/file/index.html b/reference/table/file/index.html new file mode 100644 index 00000000..c6f434ba --- /dev/null +++ b/reference/table/file/index.html @@ -0,0 +1,714 @@ + + + + + + + + +File - maddy + + + +
+ +
+ +
+ +
+ +

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, secondvalue
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/table/regexp/index.html b/reference/table/regexp/index.html new file mode 100644 index 00000000..62717b27 --- /dev/null +++ b/reference/table/regexp/index.html @@ -0,0 +1,708 @@ + + + + + + + + +Regexp rewrite table - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: 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.

+

Syntax: case_insensitive boolean
+Default: yes

+

Whether to make matching case-insensitive.

+

Syntax: 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 { }
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/table/sql_query/index.html b/reference/table/sql_query/index.html new file mode 100644 index 00000000..86839500 --- /dev/null +++ b/reference/table/sql_query/index.html @@ -0,0 +1,749 @@ + + + + + + + + +SQL query mapping - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: driver driver name
+REQUIRED

+

Driver to use to access the database.

+

Supported drivers: postgres, sqlite3 (if compiled with C support)

+

Syntax: 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

+

Syntax: 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.

+

Syntax: 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"
+}
+
+ +

Syntax: 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

+

Syntax: add query
+Syntax: list query
+Syntax: set query
+Syntax: 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).

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/table/static/index.html b/reference/table/static/index.html new file mode 100644 index 00000000..971c63ba --- /dev/null +++ b/reference/table/static/index.html @@ -0,0 +1,679 @@ + + + + + + + + +Static table - maddy + + + +
+ +
+ +
+ +
+ +

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

+

Syntax: entry key _value_

+

Add an entry to the table.

+

If the same key is used multiple times, the last one takes effect.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/targets/queue/index.html b/reference/targets/queue/index.html new file mode 100644 index 00000000..a8a2627c --- /dev/null +++ b/reference/targets/queue/index.html @@ -0,0 +1,730 @@ + + + + + + + + +Local queue - maddy + + + +
+ +
+ +
+ +
+ +

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
+}
+
+ +

Syntax: target block_name
+Default: not specified

+

REQUIRED.

+

Delivery target to use for final delivery.

+

Syntax: location directory
+Default: StateDirectory/configuration_block_name

+

File system directory to use to store queued messages. +Relative paths are relative to the StateDirectory.

+

Syntax: 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.

+

Syntax: max_tries integer
+Default: 20

+

Attempt delivery up to integer times. Note that no more attempts will be done +is permanent error occured during previous attempt.

+

Delay before the next attempt will be increased exponentally 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, ...

+

Syntax: bounce { ... }
+Default: not specified

+

This configuration contains pipeline configuration to be used for generated DSN +(Delivery Status Notifiaction) 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.

+

Syntax: 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.

+

Syntax: debug boolean
+Default: no

+

Enable verbose logging.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/targets/remote/index.html b/reference/targets/remote/index.html new file mode 100644 index 00000000..2317e5cd --- /dev/null +++ b/reference/targets/remote/index.html @@ -0,0 +1,855 @@ + + + + + + + + +Remote MX delivery - maddy + + + +
+ +
+ +
+ +
+ +

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
+}
+
+ +

Syntax: 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.

+

Syntax: limits config block
+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.

+

Syntax: local_ip IP address
+Default: empty

+

Choose the local IP to bind for outbound SMTP connections.

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: submission_timeout duration
+Default: 12m

+

Time to wait after the entire message is sent (after "final dot").

+

RFC 5321 recommends 10 minutes.

+

Syntax: debug boolean
+Default: global directive value

+

Enable verbose logging.

+

Syntax: 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).

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: conn_max_idle_count integer
+Default: 10

+

Max. amount of idle connections per recipient domains to keep in cache.

+

Syntax: conn_max_idle_time integer
+Default: 150 (2.5 min)

+

Amount of time the idle connection is still considered potentially usable.

+

Security policies

+

Syntax: mx_auth config block
+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 numberous 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 { ... }
+}
+
+ +

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
+}
+
+ +

Syntax: 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.

+

Syntax: 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 reslies on +the upstream resolver to do so by causing lookup to fail when verification +fails and setting the AD flag for signed and verfified 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'.

+

Syntax: min_tls_level none|encrypted|authenticated
+Default: none

+

Set the minimal TLS security level required for all outbound messages.

+

See Security levels page for details.

+

Syntax: 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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/targets/smtp/index.html b/reference/targets/smtp/index.html new file mode 100644 index 00000000..5414b0be --- /dev/null +++ b/reference/targets/smtp/index.html @@ -0,0 +1,753 @@ + + + + + + + + +SMTP & LMTP transparent forwarding - maddy + + + +
+ +
+ +
+ +
+ +

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
+}
+
+ +

Syntax: debug boolean
+Default: global directive value

+

Enable verbose logging.

+

Syntax: tls_client { ... }
+Default: not specified

+

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

+

Syntax: attempt_starttls boolean
+Default: yes (no for target.lmtp)

+

Attempt to use STARTTLS if it is supported by the remote server. +If TLS handshake fails, connection will be retried without STARTTLS +unless 'require_tls' is also specified.

+

Syntax: require_tls boolean
+Default: no

+

Refuse to pass messages over plain-text connections.

+

Syntax:
+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.

+

Syntax: targets endpoints...
+Default: not specified

+

REQUIRED.

+

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).

+

Syntax: connect_timeout duration
+Default: 5m

+

Same as for target.remote.

+

Syntax: command_timeout duration
+Default: 5m

+

Same as for target.remote.

+

Syntax: submission_timeout duration
+Default: 12m

+

Same as for target.remote.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/tls-acme/index.html b/reference/tls-acme/index.html new file mode 100644 index 00000000..7063fce5 --- /dev/null +++ b/reference/tls-acme/index.html @@ -0,0 +1,866 @@ + + + + + + + + +Automatic certificate management via ACME - maddy + + + +
+ +
+ +
+ +
+ +

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 put-your-email-here@example.org
+    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 &local_tls
+
+ +

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

+
tls.loader.acme local_tls {
+    email maddy-acme@example.org
+    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 test@maddy.invalid
+    agreed off
+    challenge dns-01
+    dns ...
+}
+
+ +

Syntax: debug boolean
+Default: global directive value

+

Enable debug logging.

+

Syntax: hostname str
+Default: global directive value

+

Domain name to issue certificate for. Required.

+

Syntax: store_path path
+Default: state_dir/acme

+

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

+

Syntax: ca url
+Default: Let's Encrypt production CA

+

URL of ACME directory to use.

+

Syntax: 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.

+

Syntax: email str
+Default: not set

+

Email to pass while registering an ACME account.

+

Syntax: agreed boolean
+Default: false

+

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

+

Syntax: 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 "..."
+}
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/reference/tls/index.html b/reference/tls/index.html new file mode 100644 index 00000000..47717067 --- /dev/null +++ b/reference/tls/index.html @@ -0,0 +1,797 @@ + + + + + + + + +TLS configuration - maddy + + + +
+ +
+ +
+ +
+ +

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.

+

Syntax:
+protocols min_version max_version
+protocols 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

+

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

+

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

+

Valid values:

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

Syntax: 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
+}
+
+ +

Syntax:
+protocols min_version max_version
+protocols 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

+

Syntax: 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.

+

Syntax: 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.

+

Syntax: root_ca paths...
+Default: system CA pool

+

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

+

Syntax:
+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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/search.html b/search.html new file mode 100644 index 00000000..17d5a766 --- /dev/null +++ b/search.html @@ -0,0 +1,666 @@ + + + + + + + + +maddy + + + +
+ +
+ +
+ +
+ +

Search Results

+ + + +
+Searching... +
+ + +
+ +
+ + + + + + + +
+ +
+
+ +
+ + + + + + + + + + + diff --git a/search/lunr.js b/search/lunr.js new file mode 100644 index 00000000..6aa370fb --- /dev/null +++ b/search/lunr.js @@ -0,0 +1,3475 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ + +;(function(){ + +/** + * A convenience function for configuring and constructing + * a new lunr Index. + * + * A lunr.Builder instance is created and the pipeline setup + * with a trimmer, stop word filter and stemmer. + * + * This builder object is yielded to the configuration function + * that is passed as a parameter, allowing the list of fields + * and other builder parameters to be customised. + * + * All documents _must_ be added within the passed config function. + * + * @example + * var idx = lunr(function () { + * this.field('title') + * this.field('body') + * this.ref('id') + * + * documents.forEach(function (doc) { + * this.add(doc) + * }, this) + * }) + * + * @see {@link lunr.Builder} + * @see {@link lunr.Pipeline} + * @see {@link lunr.trimmer} + * @see {@link lunr.stopWordFilter} + * @see {@link lunr.stemmer} + * @namespace {function} lunr + */ +var lunr = function (config) { + var builder = new lunr.Builder + + builder.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + builder.searchPipeline.add( + lunr.stemmer + ) + + config.call(builder, builder) + return builder.build() +} + +lunr.version = "2.3.9" +/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A namespace containing utils for the rest of the lunr library + * @namespace lunr.utils + */ +lunr.utils = {} + +/** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf lunr.utils + * @function + */ +lunr.utils.warn = (function (global) { + /* eslint-disable no-console */ + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + /* eslint-enable no-console */ +})(this) + +/** + * Convert an object to a string. + * + * In the case of `null` and `undefined` the function returns + * the empty string, in all other cases the result of calling + * `toString` on the passed object is returned. + * + * @param {Any} obj The object to convert to a string. + * @return {String} string representation of the passed object. + * @memberOf lunr.utils + */ +lunr.utils.asString = function (obj) { + if (obj === void 0 || obj === null) { + return "" + } else { + return obj.toString() + } +} + +/** + * Clones an object. + * + * Will create a copy of an existing object such that any mutations + * on the copy cannot affect the original. + * + * Only shallow objects are supported, passing a nested object to this + * function will cause a TypeError. + * + * Objects with primitives, and arrays of primitives are supported. + * + * @param {Object} obj The object to clone. + * @return {Object} a clone of the passed object. + * @throws {TypeError} when a nested object is passed. + * @memberOf Utils + */ +lunr.utils.clone = function (obj) { + if (obj === null || obj === undefined) { + return obj + } + + var clone = Object.create(null), + keys = Object.keys(obj) + + for (var i = 0; i < keys.length; i++) { + var key = keys[i], + val = obj[key] + + if (Array.isArray(val)) { + clone[key] = val.slice() + continue + } + + if (typeof val === 'string' || + typeof val === 'number' || + typeof val === 'boolean') { + clone[key] = val + continue + } + + throw new TypeError("clone is not deep and does not support nested objects") + } + + return clone +} +lunr.FieldRef = function (docRef, fieldName, stringValue) { + this.docRef = docRef + this.fieldName = fieldName + this._stringValue = stringValue +} + +lunr.FieldRef.joiner = "/" + +lunr.FieldRef.fromString = function (s) { + var n = s.indexOf(lunr.FieldRef.joiner) + + if (n === -1) { + throw "malformed field ref string" + } + + var fieldRef = s.slice(0, n), + docRef = s.slice(n + 1) + + return new lunr.FieldRef (docRef, fieldRef, s) +} + +lunr.FieldRef.prototype.toString = function () { + if (this._stringValue == undefined) { + this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef + } + + return this._stringValue +} +/*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A lunr set. + * + * @constructor + */ +lunr.Set = function (elements) { + this.elements = Object.create(null) + + if (elements) { + this.length = elements.length + + for (var i = 0; i < this.length; i++) { + this.elements[elements[i]] = true + } + } else { + this.length = 0 + } +} + +/** + * A complete set that contains all elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.complete = { + intersect: function (other) { + return other + }, + + union: function () { + return this + }, + + contains: function () { + return true + } +} + +/** + * An empty set that contains no elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.empty = { + intersect: function () { + return this + }, + + union: function (other) { + return other + }, + + contains: function () { + return false + } +} + +/** + * Returns true if this set contains the specified object. + * + * @param {object} object - Object whose presence in this set is to be tested. + * @returns {boolean} - True if this set contains the specified object. + */ +lunr.Set.prototype.contains = function (object) { + return !!this.elements[object] +} + +/** + * Returns a new set containing only the elements that are present in both + * this set and the specified set. + * + * @param {lunr.Set} other - set to intersect with this set. + * @returns {lunr.Set} a new set that is the intersection of this and the specified set. + */ + +lunr.Set.prototype.intersect = function (other) { + var a, b, elements, intersection = [] + + if (other === lunr.Set.complete) { + return this + } + + if (other === lunr.Set.empty) { + return other + } + + if (this.length < other.length) { + a = this + b = other + } else { + a = other + b = this + } + + elements = Object.keys(a.elements) + + for (var i = 0; i < elements.length; i++) { + var element = elements[i] + if (element in b.elements) { + intersection.push(element) + } + } + + return new lunr.Set (intersection) +} + +/** + * Returns a new set combining the elements of this and the specified set. + * + * @param {lunr.Set} other - set to union with this set. + * @return {lunr.Set} a new set that is the union of this and the specified set. + */ + +lunr.Set.prototype.union = function (other) { + if (other === lunr.Set.complete) { + return lunr.Set.complete + } + + if (other === lunr.Set.empty) { + return this + } + + return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements))) +} +/** + * A function to calculate the inverse document frequency for + * a posting. This is shared between the builder and the index + * + * @private + * @param {object} posting - The posting for a given term + * @param {number} documentCount - The total number of documents. + */ +lunr.idf = function (posting, documentCount) { + var documentsWithTerm = 0 + + for (var fieldName in posting) { + if (fieldName == '_index') continue // Ignore the term index, its not a field + documentsWithTerm += Object.keys(posting[fieldName]).length + } + + var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5) + + return Math.log(1 + Math.abs(x)) +} + +/** + * A token wraps a string representation of a token + * as it is passed through the text processing pipeline. + * + * @constructor + * @param {string} [str=''] - The string token being wrapped. + * @param {object} [metadata={}] - Metadata associated with this token. + */ +lunr.Token = function (str, metadata) { + this.str = str || "" + this.metadata = metadata || {} +} + +/** + * Returns the token string that is being wrapped by this object. + * + * @returns {string} + */ +lunr.Token.prototype.toString = function () { + return this.str +} + +/** + * A token update function is used when updating or optionally + * when cloning a token. + * + * @callback lunr.Token~updateFunction + * @param {string} str - The string representation of the token. + * @param {Object} metadata - All metadata associated with this token. + */ + +/** + * Applies the given function to the wrapped string token. + * + * @example + * token.update(function (str, metadata) { + * return str.toUpperCase() + * }) + * + * @param {lunr.Token~updateFunction} fn - A function to apply to the token string. + * @returns {lunr.Token} + */ +lunr.Token.prototype.update = function (fn) { + this.str = fn(this.str, this.metadata) + return this +} + +/** + * Creates a clone of this token. Optionally a function can be + * applied to the cloned token. + * + * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token. + * @returns {lunr.Token} + */ +lunr.Token.prototype.clone = function (fn) { + fn = fn || function (s) { return s } + return new lunr.Token (fn(this.str, this.metadata), this.metadata) +} +/*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A function for splitting a string into tokens ready to be inserted into + * the search index. Uses `lunr.tokenizer.separator` to split strings, change + * the value of this property to change how strings are split into tokens. + * + * This tokenizer will convert its parameter to a string by calling `toString` and + * then will split this string on the character in `lunr.tokenizer.separator`. + * Arrays will have their elements converted to strings and wrapped in a lunr.Token. + * + * Optional metadata can be passed to the tokenizer, this metadata will be cloned and + * added as metadata to every token that is created from the object to be tokenized. + * + * @static + * @param {?(string|object|object[])} obj - The object to convert into tokens + * @param {?object} metadata - Optional metadata to associate with every token + * @returns {lunr.Token[]} + * @see {@link lunr.Pipeline} + */ +lunr.tokenizer = function (obj, metadata) { + if (obj == null || obj == undefined) { + return [] + } + + if (Array.isArray(obj)) { + return obj.map(function (t) { + return new lunr.Token( + lunr.utils.asString(t).toLowerCase(), + lunr.utils.clone(metadata) + ) + }) + } + + var str = obj.toString().toLowerCase(), + len = str.length, + tokens = [] + + for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) { + var char = str.charAt(sliceEnd), + sliceLength = sliceEnd - sliceStart + + if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) { + + if (sliceLength > 0) { + var tokenMetadata = lunr.utils.clone(metadata) || {} + tokenMetadata["position"] = [sliceStart, sliceLength] + tokenMetadata["index"] = tokens.length + + tokens.push( + new lunr.Token ( + str.slice(sliceStart, sliceEnd), + tokenMetadata + ) + ) + } + + sliceStart = sliceEnd + 1 + } + + } + + return tokens +} + +/** + * The separator used to split a string into tokens. Override this property to change the behaviour of + * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. + * + * @static + * @see lunr.tokenizer + */ +lunr.tokenizer.separator = /[\s\-]+/ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { + this._stack = [] +} + +lunr.Pipeline.registeredFunctions = Object.create(null) + +/** + * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token + * string as well as all known metadata. A pipeline function can mutate the token string + * or mutate (or add) metadata for a given token. + * + * A pipeline function can indicate that the passed token should be discarded by returning + * null, undefined or an empty string. This token will not be passed to any downstream pipeline + * functions and will not be added to the index. + * + * Multiple tokens can be returned by returning an array of tokens. Each token will be passed + * to any downstream pipeline functions and all will returned tokens will be added to the index. + * + * Any number of pipeline functions may be chained together using a lunr.Pipeline. + * + * @interface lunr.PipelineFunction + * @param {lunr.Token} token - A token from the document being processed. + * @param {number} i - The index of this token in the complete list of tokens for this document/field. + * @param {lunr.Token[]} tokens - All tokens for this document/field. + * @returns {(?lunr.Token|lunr.Token[])} + */ + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @param {String} label - The label to register this function with + */ +lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @private + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised - The serialised pipeline to load. + * @returns {lunr.Pipeline} + */ +lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error('Cannot load unregistered function: ' + fnName) + } + }) + + return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline. + */ +lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + pos = pos + 1 + this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {lunr.PipelineFunction} fn The function to remove from the pipeline. + */ +lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + if (pos == -1) { + return + } + + this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + */ +lunr.Pipeline.prototype.run = function (tokens) { + var stackLength = this._stack.length + + for (var i = 0; i < stackLength; i++) { + var fn = this._stack[i] + var memo = [] + + for (var j = 0; j < tokens.length; j++) { + var result = fn(tokens[j], j, tokens) + + if (result === null || result === void 0 || result === '') continue + + if (Array.isArray(result)) { + for (var k = 0; k < result.length; k++) { + memo.push(result[k]) + } + } else { + memo.push(result) + } + } + + tokens = memo + } + + return tokens +} + +/** + * Convenience method for passing a string through a pipeline and getting + * strings out. This method takes care of wrapping the passed string in a + * token and mapping the resulting tokens back to strings. + * + * @param {string} str - The string to pass through the pipeline. + * @param {?object} metadata - Optional metadata to associate with the token + * passed to the pipeline. + * @returns {string[]} + */ +lunr.Pipeline.prototype.runString = function (str, metadata) { + var token = new lunr.Token (str, metadata) + + return this.run([token]).map(function (t) { + return t.toString() + }) +} + +/** + * Resets the pipeline by removing any existing processors. + * + */ +lunr.Pipeline.prototype.reset = function () { + this._stack = [] +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + */ +lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) +} +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A vector is used to construct the vector space of documents and queries. These + * vectors support operations to determine the similarity between two documents or + * a document and a query. + * + * Normally no parameters are required for initializing a vector, but in the case of + * loading a previously dumped vector the raw elements can be provided to the constructor. + * + * For performance reasons vectors are implemented with a flat array, where an elements + * index is immediately followed by its value. E.g. [index, value, index, value]. This + * allows the underlying array to be as sparse as possible and still offer decent + * performance when being used for vector calculations. + * + * @constructor + * @param {Number[]} [elements] - The flat list of element index and element value pairs. + */ +lunr.Vector = function (elements) { + this._magnitude = 0 + this.elements = elements || [] +} + + +/** + * Calculates the position within the vector to insert a given index. + * + * This is used internally by insert and upsert. If there are duplicate indexes then + * the position is returned as if the value for that index were to be updated, but it + * is the callers responsibility to check whether there is a duplicate at that index + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @returns {Number} + */ +lunr.Vector.prototype.positionForIndex = function (index) { + // For an empty vector the tuple can be inserted at the beginning + if (this.elements.length == 0) { + return 0 + } + + var start = 0, + end = this.elements.length / 2, + sliceLength = end - start, + pivotPoint = Math.floor(sliceLength / 2), + pivotIndex = this.elements[pivotPoint * 2] + + while (sliceLength > 1) { + if (pivotIndex < index) { + start = pivotPoint + } + + if (pivotIndex > index) { + end = pivotPoint + } + + if (pivotIndex == index) { + break + } + + sliceLength = end - start + pivotPoint = start + Math.floor(sliceLength / 2) + pivotIndex = this.elements[pivotPoint * 2] + } + + if (pivotIndex == index) { + return pivotPoint * 2 + } + + if (pivotIndex > index) { + return pivotPoint * 2 + } + + if (pivotIndex < index) { + return (pivotPoint + 1) * 2 + } +} + +/** + * Inserts an element at an index within the vector. + * + * Does not allow duplicates, will throw an error if there is already an entry + * for this index. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + */ +lunr.Vector.prototype.insert = function (insertIdx, val) { + this.upsert(insertIdx, val, function () { + throw "duplicate index" + }) +} + +/** + * Inserts or updates an existing index within the vector. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + * @param {function} fn - A function that is called for updates, the existing value and the + * requested value are passed as arguments + */ +lunr.Vector.prototype.upsert = function (insertIdx, val, fn) { + this._magnitude = 0 + var position = this.positionForIndex(insertIdx) + + if (this.elements[position] == insertIdx) { + this.elements[position + 1] = fn(this.elements[position + 1], val) + } else { + this.elements.splice(position, 0, insertIdx, val) + } +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + */ +lunr.Vector.prototype.magnitude = function () { + if (this._magnitude) return this._magnitude + + var sumOfSquares = 0, + elementsLength = this.elements.length + + for (var i = 1; i < elementsLength; i += 2) { + var val = this.elements[i] + sumOfSquares += val * val + } + + return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector - The vector to compute the dot product with. + * @returns {Number} + */ +lunr.Vector.prototype.dot = function (otherVector) { + var dotProduct = 0, + a = this.elements, b = otherVector.elements, + aLen = a.length, bLen = b.length, + aVal = 0, bVal = 0, + i = 0, j = 0 + + while (i < aLen && j < bLen) { + aVal = a[i], bVal = b[j] + if (aVal < bVal) { + i += 2 + } else if (aVal > bVal) { + j += 2 + } else if (aVal == bVal) { + dotProduct += a[i + 1] * b[j + 1] + i += 2 + j += 2 + } + } + + return dotProduct +} + +/** + * Calculates the similarity between this vector and another vector. + * + * @param {lunr.Vector} otherVector - The other vector to calculate the + * similarity with. + * @returns {Number} + */ +lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / this.magnitude() || 0 +} + +/** + * Converts the vector to an array of the elements within the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toArray = function () { + var output = new Array (this.elements.length / 2) + + for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) { + output[j] = this.elements[i] + } + + return output +} + +/** + * A JSON serializable representation of the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toJSON = function () { + return this.elements +} +/* eslint-disable */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartarus.org/~martin + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token - The string to stem + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + * @function + */ +lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return function (token) { + return token.update(porterStemmer); + } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.generateStopWordFilter builds a stopWordFilter function from the provided + * list of stop words. + * + * The built in lunr.stopWordFilter is built using this generator and can be used + * to generate custom stopWordFilters for applications or non English languages. + * + * @function + * @param {Array} token The token to pass through the filter + * @returns {lunr.PipelineFunction} + * @see lunr.Pipeline + * @see lunr.stopWordFilter + */ +lunr.generateStopWordFilter = function (stopWords) { + var words = stopWords.reduce(function (memo, stopWord) { + memo[stopWord] = stopWord + return memo + }, {}) + + return function (token) { + if (token && words[token.toString()] !== token.toString()) return token + } +} + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @function + * @implements {lunr.PipelineFunction} + * @params {lunr.Token} token - A token to check for being a stop word. + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + */ +lunr.stopWordFilter = lunr.generateStopWordFilter([ + 'a', + 'able', + 'about', + 'across', + 'after', + 'all', + 'almost', + 'also', + 'am', + 'among', + 'an', + 'and', + 'any', + 'are', + 'as', + 'at', + 'be', + 'because', + 'been', + 'but', + 'by', + 'can', + 'cannot', + 'could', + 'dear', + 'did', + 'do', + 'does', + 'either', + 'else', + 'ever', + 'every', + 'for', + 'from', + 'get', + 'got', + 'had', + 'has', + 'have', + 'he', + 'her', + 'hers', + 'him', + 'his', + 'how', + 'however', + 'i', + 'if', + 'in', + 'into', + 'is', + 'it', + 'its', + 'just', + 'least', + 'let', + 'like', + 'likely', + 'may', + 'me', + 'might', + 'most', + 'must', + 'my', + 'neither', + 'no', + 'nor', + 'not', + 'of', + 'off', + 'often', + 'on', + 'only', + 'or', + 'other', + 'our', + 'own', + 'rather', + 'said', + 'say', + 'says', + 'she', + 'should', + 'since', + 'so', + 'some', + 'than', + 'that', + 'the', + 'their', + 'them', + 'then', + 'there', + 'these', + 'they', + 'this', + 'tis', + 'to', + 'too', + 'twas', + 'us', + 'wants', + 'was', + 'we', + 'were', + 'what', + 'when', + 'where', + 'which', + 'while', + 'who', + 'whom', + 'why', + 'will', + 'with', + 'would', + 'yet', + 'you', + 'your' +]) + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the beginning and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token The token to pass through the filter + * @returns {lunr.Token} + * @see lunr.Pipeline + */ +lunr.trimmer = function (token) { + return token.update(function (s) { + return s.replace(/^\W+/, '').replace(/\W+$/, '') + }) +} + +lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A token set is used to store the unique list of all tokens + * within an index. Token sets are also used to represent an + * incoming query to the index, this query token set and index + * token set are then intersected to find which tokens to look + * up in the inverted index. + * + * A token set can hold multiple tokens, as in the case of the + * index token set, or it can hold a single token as in the + * case of a simple query token set. + * + * Additionally token sets are used to perform wildcard matching. + * Leading, contained and trailing wildcards are supported, and + * from this edit distance matching can also be provided. + * + * Token sets are implemented as a minimal finite state automata, + * where both common prefixes and suffixes are shared between tokens. + * This helps to reduce the space used for storing the token set. + * + * @constructor + */ +lunr.TokenSet = function () { + this.final = false + this.edges = {} + this.id = lunr.TokenSet._nextId + lunr.TokenSet._nextId += 1 +} + +/** + * Keeps track of the next, auto increment, identifier to assign + * to a new tokenSet. + * + * TokenSets require a unique identifier to be correctly minimised. + * + * @private + */ +lunr.TokenSet._nextId = 1 + +/** + * Creates a TokenSet instance from the given sorted array of words. + * + * @param {String[]} arr - A sorted array of strings to create the set from. + * @returns {lunr.TokenSet} + * @throws Will throw an error if the input array is not sorted. + */ +lunr.TokenSet.fromArray = function (arr) { + var builder = new lunr.TokenSet.Builder + + for (var i = 0, len = arr.length; i < len; i++) { + builder.insert(arr[i]) + } + + builder.finish() + return builder.root +} + +/** + * Creates a token set from a query clause. + * + * @private + * @param {Object} clause - A single clause from lunr.Query. + * @param {string} clause.term - The query clause term. + * @param {number} [clause.editDistance] - The optional edit distance for the term. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromClause = function (clause) { + if ('editDistance' in clause) { + return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance) + } else { + return lunr.TokenSet.fromString(clause.term) + } +} + +/** + * Creates a token set representing a single string with a specified + * edit distance. + * + * Insertions, deletions, substitutions and transpositions are each + * treated as an edit distance of 1. + * + * Increasing the allowed edit distance will have a dramatic impact + * on the performance of both creating and intersecting these TokenSets. + * It is advised to keep the edit distance less than 3. + * + * @param {string} str - The string to create the token set from. + * @param {number} editDistance - The allowed edit distance to match. + * @returns {lunr.Vector} + */ +lunr.TokenSet.fromFuzzyString = function (str, editDistance) { + var root = new lunr.TokenSet + + var stack = [{ + node: root, + editsRemaining: editDistance, + str: str + }] + + while (stack.length) { + var frame = stack.pop() + + // no edit + if (frame.str.length > 0) { + var char = frame.str.charAt(0), + noEditNode + + if (char in frame.node.edges) { + noEditNode = frame.node.edges[char] + } else { + noEditNode = new lunr.TokenSet + frame.node.edges[char] = noEditNode + } + + if (frame.str.length == 1) { + noEditNode.final = true + } + + stack.push({ + node: noEditNode, + editsRemaining: frame.editsRemaining, + str: frame.str.slice(1) + }) + } + + if (frame.editsRemaining == 0) { + continue + } + + // insertion + if ("*" in frame.node.edges) { + var insertionNode = frame.node.edges["*"] + } else { + var insertionNode = new lunr.TokenSet + frame.node.edges["*"] = insertionNode + } + + if (frame.str.length == 0) { + insertionNode.final = true + } + + stack.push({ + node: insertionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str + }) + + // deletion + // can only do a deletion if we have enough edits remaining + // and if there are characters left to delete in the string + if (frame.str.length > 1) { + stack.push({ + node: frame.node, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // deletion + // just removing the last character from the str + if (frame.str.length == 1) { + frame.node.final = true + } + + // substitution + // can only do a substitution if we have enough edits remaining + // and if there are characters left to substitute + if (frame.str.length >= 1) { + if ("*" in frame.node.edges) { + var substitutionNode = frame.node.edges["*"] + } else { + var substitutionNode = new lunr.TokenSet + frame.node.edges["*"] = substitutionNode + } + + if (frame.str.length == 1) { + substitutionNode.final = true + } + + stack.push({ + node: substitutionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // transposition + // can only do a transposition if there are edits remaining + // and there are enough characters to transpose + if (frame.str.length > 1) { + var charA = frame.str.charAt(0), + charB = frame.str.charAt(1), + transposeNode + + if (charB in frame.node.edges) { + transposeNode = frame.node.edges[charB] + } else { + transposeNode = new lunr.TokenSet + frame.node.edges[charB] = transposeNode + } + + if (frame.str.length == 1) { + transposeNode.final = true + } + + stack.push({ + node: transposeNode, + editsRemaining: frame.editsRemaining - 1, + str: charA + frame.str.slice(2) + }) + } + } + + return root +} + +/** + * Creates a TokenSet from a string. + * + * The string may contain one or more wildcard characters (*) + * that will allow wildcard matching when intersecting with + * another TokenSet. + * + * @param {string} str - The string to create a TokenSet from. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromString = function (str) { + var node = new lunr.TokenSet, + root = node + + /* + * Iterates through all characters within the passed string + * appending a node for each character. + * + * When a wildcard character is found then a self + * referencing edge is introduced to continually match + * any number of any characters. + */ + for (var i = 0, len = str.length; i < len; i++) { + var char = str[i], + final = (i == len - 1) + + if (char == "*") { + node.edges[char] = node + node.final = final + + } else { + var next = new lunr.TokenSet + next.final = final + + node.edges[char] = next + node = next + } + } + + return root +} + +/** + * Converts this TokenSet into an array of strings + * contained within the TokenSet. + * + * This is not intended to be used on a TokenSet that + * contains wildcards, in these cases the results are + * undefined and are likely to cause an infinite loop. + * + * @returns {string[]} + */ +lunr.TokenSet.prototype.toArray = function () { + var words = [] + + var stack = [{ + prefix: "", + node: this + }] + + while (stack.length) { + var frame = stack.pop(), + edges = Object.keys(frame.node.edges), + len = edges.length + + if (frame.node.final) { + /* In Safari, at this point the prefix is sometimes corrupted, see: + * https://github.com/olivernn/lunr.js/issues/279 Calling any + * String.prototype method forces Safari to "cast" this string to what + * it's supposed to be, fixing the bug. */ + frame.prefix.charAt(0) + words.push(frame.prefix) + } + + for (var i = 0; i < len; i++) { + var edge = edges[i] + + stack.push({ + prefix: frame.prefix.concat(edge), + node: frame.node.edges[edge] + }) + } + } + + return words +} + +/** + * Generates a string representation of a TokenSet. + * + * This is intended to allow TokenSets to be used as keys + * in objects, largely to aid the construction and minimisation + * of a TokenSet. As such it is not designed to be a human + * friendly representation of the TokenSet. + * + * @returns {string} + */ +lunr.TokenSet.prototype.toString = function () { + // NOTE: Using Object.keys here as this.edges is very likely + // to enter 'hash-mode' with many keys being added + // + // avoiding a for-in loop here as it leads to the function + // being de-optimised (at least in V8). From some simple + // benchmarks the performance is comparable, but allowing + // V8 to optimize may mean easy performance wins in the future. + + if (this._str) { + return this._str + } + + var str = this.final ? '1' : '0', + labels = Object.keys(this.edges).sort(), + len = labels.length + + for (var i = 0; i < len; i++) { + var label = labels[i], + node = this.edges[label] + + str = str + label + node.id + } + + return str +} + +/** + * Returns a new TokenSet that is the intersection of + * this TokenSet and the passed TokenSet. + * + * This intersection will take into account any wildcards + * contained within the TokenSet. + * + * @param {lunr.TokenSet} b - An other TokenSet to intersect with. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.prototype.intersect = function (b) { + var output = new lunr.TokenSet, + frame = undefined + + var stack = [{ + qNode: b, + output: output, + node: this + }] + + while (stack.length) { + frame = stack.pop() + + // NOTE: As with the #toString method, we are using + // Object.keys and a for loop instead of a for-in loop + // as both of these objects enter 'hash' mode, causing + // the function to be de-optimised in V8 + var qEdges = Object.keys(frame.qNode.edges), + qLen = qEdges.length, + nEdges = Object.keys(frame.node.edges), + nLen = nEdges.length + + for (var q = 0; q < qLen; q++) { + var qEdge = qEdges[q] + + for (var n = 0; n < nLen; n++) { + var nEdge = nEdges[n] + + if (nEdge == qEdge || qEdge == '*') { + var node = frame.node.edges[nEdge], + qNode = frame.qNode.edges[qEdge], + final = node.final && qNode.final, + next = undefined + + if (nEdge in frame.output.edges) { + // an edge already exists for this character + // no need to create a new node, just set the finality + // bit unless this node is already final + next = frame.output.edges[nEdge] + next.final = next.final || final + + } else { + // no edge exists yet, must create one + // set the finality bit and insert it + // into the output + next = new lunr.TokenSet + next.final = final + frame.output.edges[nEdge] = next + } + + stack.push({ + qNode: qNode, + output: next, + node: node + }) + } + } + } + } + + return output +} +lunr.TokenSet.Builder = function () { + this.previousWord = "" + this.root = new lunr.TokenSet + this.uncheckedNodes = [] + this.minimizedNodes = {} +} + +lunr.TokenSet.Builder.prototype.insert = function (word) { + var node, + commonPrefix = 0 + + if (word < this.previousWord) { + throw new Error ("Out of order word insertion") + } + + for (var i = 0; i < word.length && i < this.previousWord.length; i++) { + if (word[i] != this.previousWord[i]) break + commonPrefix++ + } + + this.minimize(commonPrefix) + + if (this.uncheckedNodes.length == 0) { + node = this.root + } else { + node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child + } + + for (var i = commonPrefix; i < word.length; i++) { + var nextNode = new lunr.TokenSet, + char = word[i] + + node.edges[char] = nextNode + + this.uncheckedNodes.push({ + parent: node, + char: char, + child: nextNode + }) + + node = nextNode + } + + node.final = true + this.previousWord = word +} + +lunr.TokenSet.Builder.prototype.finish = function () { + this.minimize(0) +} + +lunr.TokenSet.Builder.prototype.minimize = function (downTo) { + for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) { + var node = this.uncheckedNodes[i], + childKey = node.child.toString() + + if (childKey in this.minimizedNodes) { + node.parent.edges[node.char] = this.minimizedNodes[childKey] + } else { + // Cache the key for this node since + // we know it can't change anymore + node.child._str = childKey + + this.minimizedNodes[childKey] = node.child + } + + this.uncheckedNodes.pop() + } +} +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * An index contains the built index of all documents and provides a query interface + * to the index. + * + * Usually instances of lunr.Index will not be created using this constructor, instead + * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be + * used to load previously built and serialized indexes. + * + * @constructor + * @param {Object} attrs - The attributes of the built search index. + * @param {Object} attrs.invertedIndex - An index of term/field to document reference. + * @param {Object} attrs.fieldVectors - Field vectors + * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens. + * @param {string[]} attrs.fields - The names of indexed document fields. + * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms. + */ +lunr.Index = function (attrs) { + this.invertedIndex = attrs.invertedIndex + this.fieldVectors = attrs.fieldVectors + this.tokenSet = attrs.tokenSet + this.fields = attrs.fields + this.pipeline = attrs.pipeline +} + +/** + * A result contains details of a document matching a search query. + * @typedef {Object} lunr.Index~Result + * @property {string} ref - The reference of the document this result represents. + * @property {number} score - A number between 0 and 1 representing how similar this document is to the query. + * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match. + */ + +/** + * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple + * query language which itself is parsed into an instance of lunr.Query. + * + * For programmatically building queries it is advised to directly use lunr.Query, the query language + * is best used for human entered text rather than program generated text. + * + * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported + * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello' + * or 'world', though those that contain both will rank higher in the results. + * + * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can + * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding + * wildcards will increase the number of documents that will be found but can also have a negative + * impact on query performance, especially with wildcards at the beginning of a term. + * + * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term + * hello in the title field will match this query. Using a field not present in the index will lead + * to an error being thrown. + * + * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term + * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported + * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2. + * Avoid large values for edit distance to improve query performance. + * + * Each term also supports a presence modifier. By default a term's presence in document is optional, however + * this can be changed to either required or prohibited. For a term's presence to be required in a document the + * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and + * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not + * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'. + * + * To escape special characters the backslash character '\' can be used, this allows searches to include + * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead + * of attempting to apply a boost of 2 to the search term "foo". + * + * @typedef {string} lunr.Index~QueryString + * @example Simple single term query + * hello + * @example Multiple term query + * hello world + * @example term scoped to a field + * title:hello + * @example term with a boost of 10 + * hello^10 + * @example term with an edit distance of 2 + * hello~2 + * @example terms with presence modifiers + * -foo +bar baz + */ + +/** + * Performs a search against the index using lunr query syntax. + * + * Results will be returned sorted by their score, the most relevant results + * will be returned first. For details on how the score is calculated, please see + * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}. + * + * For more programmatic querying use lunr.Index#query. + * + * @param {lunr.Index~QueryString} queryString - A string containing a lunr query. + * @throws {lunr.QueryParseError} If the passed query string cannot be parsed. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.search = function (queryString) { + return this.query(function (query) { + var parser = new lunr.QueryParser(queryString, query) + parser.parse() + }) +} + +/** + * A query builder callback provides a query object to be used to express + * the query to perform on the index. + * + * @callback lunr.Index~queryBuilder + * @param {lunr.Query} query - The query object to build up. + * @this lunr.Query + */ + +/** + * Performs a query against the index using the yielded lunr.Query object. + * + * If performing programmatic queries against the index, this method is preferred + * over lunr.Index#search so as to avoid the additional query parsing overhead. + * + * A query object is yielded to the supplied function which should be used to + * express the query to be run against the index. + * + * Note that although this function takes a callback parameter it is _not_ an + * asynchronous operation, the callback is just yielded a query object to be + * customized. + * + * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.query = function (fn) { + // for each query clause + // * process terms + // * expand terms from token set + // * find matching documents and metadata + // * get document vectors + // * score documents + + var query = new lunr.Query(this.fields), + matchingFields = Object.create(null), + queryVectors = Object.create(null), + termFieldCache = Object.create(null), + requiredMatches = Object.create(null), + prohibitedMatches = Object.create(null) + + /* + * To support field level boosts a query vector is created per + * field. An empty vector is eagerly created to support negated + * queries. + */ + for (var i = 0; i < this.fields.length; i++) { + queryVectors[this.fields[i]] = new lunr.Vector + } + + fn.call(query, query) + + for (var i = 0; i < query.clauses.length; i++) { + /* + * Unless the pipeline has been disabled for this term, which is + * the case for terms with wildcards, we need to pass the clause + * term through the search pipeline. A pipeline returns an array + * of processed terms. Pipeline functions may expand the passed + * term, which means we may end up performing multiple index lookups + * for a single query term. + */ + var clause = query.clauses[i], + terms = null, + clauseMatches = lunr.Set.empty + + if (clause.usePipeline) { + terms = this.pipeline.runString(clause.term, { + fields: clause.fields + }) + } else { + terms = [clause.term] + } + + for (var m = 0; m < terms.length; m++) { + var term = terms[m] + + /* + * Each term returned from the pipeline needs to use the same query + * clause object, e.g. the same boost and or edit distance. The + * simplest way to do this is to re-use the clause object but mutate + * its term property. + */ + clause.term = term + + /* + * From the term in the clause we create a token set which will then + * be used to intersect the indexes token set to get a list of terms + * to lookup in the inverted index + */ + var termTokenSet = lunr.TokenSet.fromClause(clause), + expandedTerms = this.tokenSet.intersect(termTokenSet).toArray() + + /* + * If a term marked as required does not exist in the tokenSet it is + * impossible for the search to return any matches. We set all the field + * scoped required matches set to empty and stop examining any further + * clauses. + */ + if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = lunr.Set.empty + } + + break + } + + for (var j = 0; j < expandedTerms.length; j++) { + /* + * For each term get the posting and termIndex, this is required for + * building the query vector. + */ + var expandedTerm = expandedTerms[j], + posting = this.invertedIndex[expandedTerm], + termIndex = posting._index + + for (var k = 0; k < clause.fields.length; k++) { + /* + * For each field that this query term is scoped by (by default + * all fields are in scope) we need to get all the document refs + * that have this term in that field. + * + * The posting is the entry in the invertedIndex for the matching + * term from above. + */ + var field = clause.fields[k], + fieldPosting = posting[field], + matchingDocumentRefs = Object.keys(fieldPosting), + termField = expandedTerm + "/" + field, + matchingDocumentsSet = new lunr.Set(matchingDocumentRefs) + + /* + * if the presence of this term is required ensure that the matching + * documents are added to the set of required matches for this clause. + * + */ + if (clause.presence == lunr.Query.presence.REQUIRED) { + clauseMatches = clauseMatches.union(matchingDocumentsSet) + + if (requiredMatches[field] === undefined) { + requiredMatches[field] = lunr.Set.complete + } + } + + /* + * if the presence of this term is prohibited ensure that the matching + * documents are added to the set of prohibited matches for this field, + * creating that set if it does not yet exist. + */ + if (clause.presence == lunr.Query.presence.PROHIBITED) { + if (prohibitedMatches[field] === undefined) { + prohibitedMatches[field] = lunr.Set.empty + } + + prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet) + + /* + * Prohibited matches should not be part of the query vector used for + * similarity scoring and no metadata should be extracted so we continue + * to the next field + */ + continue + } + + /* + * The query field vector is populated using the termIndex found for + * the term and a unit value with the appropriate boost applied. + * Using upsert because there could already be an entry in the vector + * for the term we are working with. In that case we just add the scores + * together. + */ + queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b }) + + /** + * If we've already seen this term, field combo then we've already collected + * the matching documents and metadata, no need to go through all that again + */ + if (termFieldCache[termField]) { + continue + } + + for (var l = 0; l < matchingDocumentRefs.length; l++) { + /* + * All metadata for this term/field/document triple + * are then extracted and collected into an instance + * of lunr.MatchData ready to be returned in the query + * results + */ + var matchingDocumentRef = matchingDocumentRefs[l], + matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field), + metadata = fieldPosting[matchingDocumentRef], + fieldMatch + + if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) { + matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata) + } else { + fieldMatch.add(expandedTerm, field, metadata) + } + + } + + termFieldCache[termField] = true + } + } + } + + /** + * If the presence was required we need to update the requiredMatches field sets. + * We do this after all fields for the term have collected their matches because + * the clause terms presence is required in _any_ of the fields not _all_ of the + * fields. + */ + if (clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = requiredMatches[field].intersect(clauseMatches) + } + } + } + + /** + * Need to combine the field scoped required and prohibited + * matching documents into a global set of required and prohibited + * matches + */ + var allRequiredMatches = lunr.Set.complete, + allProhibitedMatches = lunr.Set.empty + + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i] + + if (requiredMatches[field]) { + allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field]) + } + + if (prohibitedMatches[field]) { + allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field]) + } + } + + var matchingFieldRefs = Object.keys(matchingFields), + results = [], + matches = Object.create(null) + + /* + * If the query is negated (contains only prohibited terms) + * we need to get _all_ fieldRefs currently existing in the + * index. This is only done when we know that the query is + * entirely prohibited terms to avoid any cost of getting all + * fieldRefs unnecessarily. + * + * Additionally, blank MatchData must be created to correctly + * populate the results. + */ + if (query.isNegated()) { + matchingFieldRefs = Object.keys(this.fieldVectors) + + for (var i = 0; i < matchingFieldRefs.length; i++) { + var matchingFieldRef = matchingFieldRefs[i] + var fieldRef = lunr.FieldRef.fromString(matchingFieldRef) + matchingFields[matchingFieldRef] = new lunr.MatchData + } + } + + for (var i = 0; i < matchingFieldRefs.length; i++) { + /* + * Currently we have document fields that match the query, but we + * need to return documents. The matchData and scores are combined + * from multiple fields belonging to the same document. + * + * Scores are calculated by field, using the query vectors created + * above, and combined into a final document score using addition. + */ + var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]), + docRef = fieldRef.docRef + + if (!allRequiredMatches.contains(docRef)) { + continue + } + + if (allProhibitedMatches.contains(docRef)) { + continue + } + + var fieldVector = this.fieldVectors[fieldRef], + score = queryVectors[fieldRef.fieldName].similarity(fieldVector), + docMatch + + if ((docMatch = matches[docRef]) !== undefined) { + docMatch.score += score + docMatch.matchData.combine(matchingFields[fieldRef]) + } else { + var match = { + ref: docRef, + score: score, + matchData: matchingFields[fieldRef] + } + matches[docRef] = match + results.push(match) + } + } + + /* + * Sort the results objects by score, highest first. + */ + return results.sort(function (a, b) { + return b.score - a.score + }) +} + +/** + * Prepares the index for JSON serialization. + * + * The schema for this JSON blob will be described in a + * separate JSON schema file. + * + * @returns {Object} + */ +lunr.Index.prototype.toJSON = function () { + var invertedIndex = Object.keys(this.invertedIndex) + .sort() + .map(function (term) { + return [term, this.invertedIndex[term]] + }, this) + + var fieldVectors = Object.keys(this.fieldVectors) + .map(function (ref) { + return [ref, this.fieldVectors[ref].toJSON()] + }, this) + + return { + version: lunr.version, + fields: this.fields, + fieldVectors: fieldVectors, + invertedIndex: invertedIndex, + pipeline: this.pipeline.toJSON() + } +} + +/** + * Loads a previously serialized lunr.Index + * + * @param {Object} serializedIndex - A previously serialized lunr.Index + * @returns {lunr.Index} + */ +lunr.Index.load = function (serializedIndex) { + var attrs = {}, + fieldVectors = {}, + serializedVectors = serializedIndex.fieldVectors, + invertedIndex = Object.create(null), + serializedInvertedIndex = serializedIndex.invertedIndex, + tokenSetBuilder = new lunr.TokenSet.Builder, + pipeline = lunr.Pipeline.load(serializedIndex.pipeline) + + if (serializedIndex.version != lunr.version) { + lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'") + } + + for (var i = 0; i < serializedVectors.length; i++) { + var tuple = serializedVectors[i], + ref = tuple[0], + elements = tuple[1] + + fieldVectors[ref] = new lunr.Vector(elements) + } + + for (var i = 0; i < serializedInvertedIndex.length; i++) { + var tuple = serializedInvertedIndex[i], + term = tuple[0], + posting = tuple[1] + + tokenSetBuilder.insert(term) + invertedIndex[term] = posting + } + + tokenSetBuilder.finish() + + attrs.fields = serializedIndex.fields + + attrs.fieldVectors = fieldVectors + attrs.invertedIndex = invertedIndex + attrs.tokenSet = tokenSetBuilder.root + attrs.pipeline = pipeline + + return new lunr.Index(attrs) +} +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Builder performs indexing on a set of documents and + * returns instances of lunr.Index ready for querying. + * + * All configuration of the index is done via the builder, the + * fields to index, the document reference, the text processing + * pipeline and document scoring parameters are all set on the + * builder before indexing. + * + * @constructor + * @property {string} _ref - Internal reference to the document reference field. + * @property {string[]} _fields - Internal reference to the document fields to index. + * @property {object} invertedIndex - The inverted index maps terms to document fields. + * @property {object} documentTermFrequencies - Keeps track of document term frequencies. + * @property {object} documentLengths - Keeps track of the length of documents added to the index. + * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing. + * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing. + * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index. + * @property {number} documentCount - Keeps track of the total number of documents indexed. + * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75. + * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2. + * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space. + * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index. + */ +lunr.Builder = function () { + this._ref = "id" + this._fields = Object.create(null) + this._documents = Object.create(null) + this.invertedIndex = Object.create(null) + this.fieldTermFrequencies = {} + this.fieldLengths = {} + this.tokenizer = lunr.tokenizer + this.pipeline = new lunr.Pipeline + this.searchPipeline = new lunr.Pipeline + this.documentCount = 0 + this._b = 0.75 + this._k1 = 1.2 + this.termIndex = 0 + this.metadataWhitelist = [] +} + +/** + * Sets the document field used as the document reference. Every document must have this field. + * The type of this field in the document should be a string, if it is not a string it will be + * coerced into a string by calling toString. + * + * The default ref is 'id'. + * + * The ref should _not_ be changed during indexing, it should be set before any documents are + * added to the index. Changing it during indexing can lead to inconsistent results. + * + * @param {string} ref - The name of the reference field in the document. + */ +lunr.Builder.prototype.ref = function (ref) { + this._ref = ref +} + +/** + * A function that is used to extract a field from a document. + * + * Lunr expects a field to be at the top level of a document, if however the field + * is deeply nested within a document an extractor function can be used to extract + * the right field for indexing. + * + * @callback fieldExtractor + * @param {object} doc - The document being added to the index. + * @returns {?(string|object|object[])} obj - The object that will be indexed for this field. + * @example Extracting a nested field + * function (doc) { return doc.nested.field } + */ + +/** + * Adds a field to the list of document fields that will be indexed. Every document being + * indexed should have this field. Null values for this field in indexed documents will + * not cause errors but will limit the chance of that document being retrieved by searches. + * + * All fields should be added before adding documents to the index. Adding fields after + * a document has been indexed will have no effect on already indexed documents. + * + * Fields can be boosted at build time. This allows terms within that field to have more + * importance when ranking search results. Use a field boost to specify that matches within + * one field are more important than other fields. + * + * @param {string} fieldName - The name of a field to index in all documents. + * @param {object} attributes - Optional attributes associated with this field. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this field. + * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document. + * @throws {RangeError} fieldName cannot contain unsupported characters '/' + */ +lunr.Builder.prototype.field = function (fieldName, attributes) { + if (/\//.test(fieldName)) { + throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'") + } + + this._fields[fieldName] = attributes || {} +} + +/** + * A parameter to tune the amount of field length normalisation that is applied when + * calculating relevance scores. A value of 0 will completely disable any normalisation + * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b + * will be clamped to the range 0 - 1. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.b = function (number) { + if (number < 0) { + this._b = 0 + } else if (number > 1) { + this._b = 1 + } else { + this._b = number + } +} + +/** + * A parameter that controls the speed at which a rise in term frequency results in term + * frequency saturation. The default value is 1.2. Setting this to a higher value will give + * slower saturation levels, a lower value will result in quicker saturation. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.k1 = function (number) { + this._k1 = number +} + +/** + * Adds a document to the index. + * + * Before adding fields to the index the index should have been fully setup, with the document + * ref and all fields to index already having been specified. + * + * The document must have a field name as specified by the ref (by default this is 'id') and + * it should have all fields defined for indexing, though null or undefined values will not + * cause errors. + * + * Entire documents can be boosted at build time. Applying a boost to a document indicates that + * this document should rank higher in search results than other documents. + * + * @param {object} doc - The document to add to the index. + * @param {object} attributes - Optional attributes associated with this document. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this document. + */ +lunr.Builder.prototype.add = function (doc, attributes) { + var docRef = doc[this._ref], + fields = Object.keys(this._fields) + + this._documents[docRef] = attributes || {} + this.documentCount += 1 + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i], + extractor = this._fields[fieldName].extractor, + field = extractor ? extractor(doc) : doc[fieldName], + tokens = this.tokenizer(field, { + fields: [fieldName] + }), + terms = this.pipeline.run(tokens), + fieldRef = new lunr.FieldRef (docRef, fieldName), + fieldTerms = Object.create(null) + + this.fieldTermFrequencies[fieldRef] = fieldTerms + this.fieldLengths[fieldRef] = 0 + + // store the length of this field for this document + this.fieldLengths[fieldRef] += terms.length + + // calculate term frequencies for this field + for (var j = 0; j < terms.length; j++) { + var term = terms[j] + + if (fieldTerms[term] == undefined) { + fieldTerms[term] = 0 + } + + fieldTerms[term] += 1 + + // add to inverted index + // create an initial posting if one doesn't exist + if (this.invertedIndex[term] == undefined) { + var posting = Object.create(null) + posting["_index"] = this.termIndex + this.termIndex += 1 + + for (var k = 0; k < fields.length; k++) { + posting[fields[k]] = Object.create(null) + } + + this.invertedIndex[term] = posting + } + + // add an entry for this term/fieldName/docRef to the invertedIndex + if (this.invertedIndex[term][fieldName][docRef] == undefined) { + this.invertedIndex[term][fieldName][docRef] = Object.create(null) + } + + // store all whitelisted metadata about this token in the + // inverted index + for (var l = 0; l < this.metadataWhitelist.length; l++) { + var metadataKey = this.metadataWhitelist[l], + metadata = term.metadata[metadataKey] + + if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) { + this.invertedIndex[term][fieldName][docRef][metadataKey] = [] + } + + this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata) + } + } + + } +} + +/** + * Calculates the average document length for this index + * + * @private + */ +lunr.Builder.prototype.calculateAverageFieldLengths = function () { + + var fieldRefs = Object.keys(this.fieldLengths), + numberOfFields = fieldRefs.length, + accumulator = {}, + documentsWithField = {} + + for (var i = 0; i < numberOfFields; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + field = fieldRef.fieldName + + documentsWithField[field] || (documentsWithField[field] = 0) + documentsWithField[field] += 1 + + accumulator[field] || (accumulator[field] = 0) + accumulator[field] += this.fieldLengths[fieldRef] + } + + var fields = Object.keys(this._fields) + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i] + accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName] + } + + this.averageFieldLength = accumulator +} + +/** + * Builds a vector space model of every document using lunr.Vector + * + * @private + */ +lunr.Builder.prototype.createFieldVectors = function () { + var fieldVectors = {}, + fieldRefs = Object.keys(this.fieldTermFrequencies), + fieldRefsLength = fieldRefs.length, + termIdfCache = Object.create(null) + + for (var i = 0; i < fieldRefsLength; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + fieldName = fieldRef.fieldName, + fieldLength = this.fieldLengths[fieldRef], + fieldVector = new lunr.Vector, + termFrequencies = this.fieldTermFrequencies[fieldRef], + terms = Object.keys(termFrequencies), + termsLength = terms.length + + + var fieldBoost = this._fields[fieldName].boost || 1, + docBoost = this._documents[fieldRef.docRef].boost || 1 + + for (var j = 0; j < termsLength; j++) { + var term = terms[j], + tf = termFrequencies[term], + termIndex = this.invertedIndex[term]._index, + idf, score, scoreWithPrecision + + if (termIdfCache[term] === undefined) { + idf = lunr.idf(this.invertedIndex[term], this.documentCount) + termIdfCache[term] = idf + } else { + idf = termIdfCache[term] + } + + score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf) + score *= fieldBoost + score *= docBoost + scoreWithPrecision = Math.round(score * 1000) / 1000 + // Converts 1.23456789 to 1.234. + // Reducing the precision so that the vectors take up less + // space when serialised. Doing it now so that they behave + // the same before and after serialisation. Also, this is + // the fastest approach to reducing a number's precision in + // JavaScript. + + fieldVector.insert(termIndex, scoreWithPrecision) + } + + fieldVectors[fieldRef] = fieldVector + } + + this.fieldVectors = fieldVectors +} + +/** + * Creates a token set of all tokens in the index using lunr.TokenSet + * + * @private + */ +lunr.Builder.prototype.createTokenSet = function () { + this.tokenSet = lunr.TokenSet.fromArray( + Object.keys(this.invertedIndex).sort() + ) +} + +/** + * Builds the index, creating an instance of lunr.Index. + * + * This completes the indexing process and should only be called + * once all documents have been added to the index. + * + * @returns {lunr.Index} + */ +lunr.Builder.prototype.build = function () { + this.calculateAverageFieldLengths() + this.createFieldVectors() + this.createTokenSet() + + return new lunr.Index({ + invertedIndex: this.invertedIndex, + fieldVectors: this.fieldVectors, + tokenSet: this.tokenSet, + fields: Object.keys(this._fields), + pipeline: this.searchPipeline + }) +} + +/** + * Applies a plugin to the index builder. + * + * A plugin is a function that is called with the index builder as its context. + * Plugins can be used to customise or extend the behaviour of the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied when building the index. + * + * The plugin function will be called with the index builder as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index builder as its context. + * + * @param {Function} plugin The plugin to apply. + */ +lunr.Builder.prototype.use = function (fn) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + fn.apply(this, args) +} +/** + * Contains and collects metadata about a matching document. + * A single instance of lunr.MatchData is returned as part of every + * lunr.Index~Result. + * + * @constructor + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + * @property {object} metadata - A cloned collection of metadata associated with this document. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData = function (term, field, metadata) { + var clonedMetadata = Object.create(null), + metadataKeys = Object.keys(metadata || {}) + + // Cloning the metadata to prevent the original + // being mutated during match data combination. + // Metadata is kept in an array within the inverted + // index so cloning the data can be done with + // Array#slice + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + clonedMetadata[key] = metadata[key].slice() + } + + this.metadata = Object.create(null) + + if (term !== undefined) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = clonedMetadata + } +} + +/** + * An instance of lunr.MatchData will be created for every term that matches a + * document. However only one instance is required in a lunr.Index~Result. This + * method combines metadata from another instance of lunr.MatchData with this + * objects metadata. + * + * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData.prototype.combine = function (otherMatchData) { + var terms = Object.keys(otherMatchData.metadata) + + for (var i = 0; i < terms.length; i++) { + var term = terms[i], + fields = Object.keys(otherMatchData.metadata[term]) + + if (this.metadata[term] == undefined) { + this.metadata[term] = Object.create(null) + } + + for (var j = 0; j < fields.length; j++) { + var field = fields[j], + keys = Object.keys(otherMatchData.metadata[term][field]) + + if (this.metadata[term][field] == undefined) { + this.metadata[term][field] = Object.create(null) + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k] + + if (this.metadata[term][field][key] == undefined) { + this.metadata[term][field][key] = otherMatchData.metadata[term][field][key] + } else { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key]) + } + + } + } + } +} + +/** + * Add metadata for a term/field pair to this instance of match data. + * + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + */ +lunr.MatchData.prototype.add = function (term, field, metadata) { + if (!(term in this.metadata)) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = metadata + return + } + + if (!(field in this.metadata[term])) { + this.metadata[term][field] = metadata + return + } + + var metadataKeys = Object.keys(metadata) + + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + + if (key in this.metadata[term][field]) { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key]) + } else { + this.metadata[term][field][key] = metadata[key] + } + } +} +/** + * A lunr.Query provides a programmatic way of defining queries to be performed + * against a {@link lunr.Index}. + * + * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method + * so the query object is pre-initialized with the right index fields. + * + * @constructor + * @property {lunr.Query~Clause[]} clauses - An array of query clauses. + * @property {string[]} allFields - An array of all available fields in a lunr.Index. + */ +lunr.Query = function (allFields) { + this.clauses = [] + this.allFields = allFields +} + +/** + * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause. + * + * This allows wildcards to be added to the beginning and end of a term without having to manually do any string + * concatenation. + * + * The wildcard constants can be bitwise combined to select both leading and trailing wildcards. + * + * @constant + * @default + * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour + * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists + * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with trailing wildcard + * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING }) + * @example query term with leading and trailing wildcard + * query.term('foo', { + * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING + * }) + */ + +lunr.Query.wildcard = new String ("*") +lunr.Query.wildcard.NONE = 0 +lunr.Query.wildcard.LEADING = 1 +lunr.Query.wildcard.TRAILING = 2 + +/** + * Constants for indicating what kind of presence a term must have in matching documents. + * + * @constant + * @enum {number} + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with required presence + * query.term('foo', { presence: lunr.Query.presence.REQUIRED }) + */ +lunr.Query.presence = { + /** + * Term's presence in a document is optional, this is the default value. + */ + OPTIONAL: 1, + + /** + * Term's presence in a document is required, documents that do not contain + * this term will not be returned. + */ + REQUIRED: 2, + + /** + * Term's presence in a document is prohibited, documents that do contain + * this term will not be returned. + */ + PROHIBITED: 3 +} + +/** + * A single clause in a {@link lunr.Query} contains a term and details on how to + * match that term against a {@link lunr.Index}. + * + * @typedef {Object} lunr.Query~Clause + * @property {string[]} fields - The fields in an index this clause should be matched against. + * @property {number} [boost=1] - Any boost that should be applied when matching this clause. + * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be. + * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline. + * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended. + * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents. + */ + +/** + * Adds a {@link lunr.Query~Clause} to this query. + * + * Unless the clause contains the fields to be matched all fields will be matched. In addition + * a default boost of 1 is applied to the clause. + * + * @param {lunr.Query~Clause} clause - The clause to add to this query. + * @see lunr.Query~Clause + * @returns {lunr.Query} + */ +lunr.Query.prototype.clause = function (clause) { + if (!('fields' in clause)) { + clause.fields = this.allFields + } + + if (!('boost' in clause)) { + clause.boost = 1 + } + + if (!('usePipeline' in clause)) { + clause.usePipeline = true + } + + if (!('wildcard' in clause)) { + clause.wildcard = lunr.Query.wildcard.NONE + } + + if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) { + clause.term = "*" + clause.term + } + + if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) { + clause.term = "" + clause.term + "*" + } + + if (!('presence' in clause)) { + clause.presence = lunr.Query.presence.OPTIONAL + } + + this.clauses.push(clause) + + return this +} + +/** + * A negated query is one in which every clause has a presence of + * prohibited. These queries require some special processing to return + * the expected results. + * + * @returns boolean + */ +lunr.Query.prototype.isNegated = function () { + for (var i = 0; i < this.clauses.length; i++) { + if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) { + return false + } + } + + return true +} + +/** + * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause} + * to the list of clauses that make up this query. + * + * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion + * to a token or token-like string should be done before calling this method. + * + * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an + * array, each term in the array will share the same options. + * + * @param {object|object[]} term - The term(s) to add to the query. + * @param {object} [options] - Any additional properties to add to the query clause. + * @returns {lunr.Query} + * @see lunr.Query#clause + * @see lunr.Query~Clause + * @example adding a single term to a query + * query.term("foo") + * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard + * query.term("foo", { + * fields: ["title"], + * boost: 10, + * wildcard: lunr.Query.wildcard.TRAILING + * }) + * @example using lunr.tokenizer to convert a string to tokens before using them as terms + * query.term(lunr.tokenizer("foo bar")) + */ +lunr.Query.prototype.term = function (term, options) { + if (Array.isArray(term)) { + term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this) + return this + } + + var clause = options || {} + clause.term = term.toString() + + this.clause(clause) + + return this +} +lunr.QueryParseError = function (message, start, end) { + this.name = "QueryParseError" + this.message = message + this.start = start + this.end = end +} + +lunr.QueryParseError.prototype = new Error +lunr.QueryLexer = function (str) { + this.lexemes = [] + this.str = str + this.length = str.length + this.pos = 0 + this.start = 0 + this.escapeCharPositions = [] +} + +lunr.QueryLexer.prototype.run = function () { + var state = lunr.QueryLexer.lexText + + while (state) { + state = state(this) + } +} + +lunr.QueryLexer.prototype.sliceString = function () { + var subSlices = [], + sliceStart = this.start, + sliceEnd = this.pos + + for (var i = 0; i < this.escapeCharPositions.length; i++) { + sliceEnd = this.escapeCharPositions[i] + subSlices.push(this.str.slice(sliceStart, sliceEnd)) + sliceStart = sliceEnd + 1 + } + + subSlices.push(this.str.slice(sliceStart, this.pos)) + this.escapeCharPositions.length = 0 + + return subSlices.join('') +} + +lunr.QueryLexer.prototype.emit = function (type) { + this.lexemes.push({ + type: type, + str: this.sliceString(), + start: this.start, + end: this.pos + }) + + this.start = this.pos +} + +lunr.QueryLexer.prototype.escapeCharacter = function () { + this.escapeCharPositions.push(this.pos - 1) + this.pos += 1 +} + +lunr.QueryLexer.prototype.next = function () { + if (this.pos >= this.length) { + return lunr.QueryLexer.EOS + } + + var char = this.str.charAt(this.pos) + this.pos += 1 + return char +} + +lunr.QueryLexer.prototype.width = function () { + return this.pos - this.start +} + +lunr.QueryLexer.prototype.ignore = function () { + if (this.start == this.pos) { + this.pos += 1 + } + + this.start = this.pos +} + +lunr.QueryLexer.prototype.backup = function () { + this.pos -= 1 +} + +lunr.QueryLexer.prototype.acceptDigitRun = function () { + var char, charCode + + do { + char = this.next() + charCode = char.charCodeAt(0) + } while (charCode > 47 && charCode < 58) + + if (char != lunr.QueryLexer.EOS) { + this.backup() + } +} + +lunr.QueryLexer.prototype.more = function () { + return this.pos < this.length +} + +lunr.QueryLexer.EOS = 'EOS' +lunr.QueryLexer.FIELD = 'FIELD' +lunr.QueryLexer.TERM = 'TERM' +lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE' +lunr.QueryLexer.BOOST = 'BOOST' +lunr.QueryLexer.PRESENCE = 'PRESENCE' + +lunr.QueryLexer.lexField = function (lexer) { + lexer.backup() + lexer.emit(lunr.QueryLexer.FIELD) + lexer.ignore() + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexTerm = function (lexer) { + if (lexer.width() > 1) { + lexer.backup() + lexer.emit(lunr.QueryLexer.TERM) + } + + lexer.ignore() + + if (lexer.more()) { + return lunr.QueryLexer.lexText + } +} + +lunr.QueryLexer.lexEditDistance = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.EDIT_DISTANCE) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexBoost = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.BOOST) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexEOS = function (lexer) { + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } +} + +// This matches the separator used when tokenising fields +// within a document. These should match otherwise it is +// not possible to search for some tokens within a document. +// +// It is possible for the user to change the separator on the +// tokenizer so it _might_ clash with any other of the special +// characters already used within the search string, e.g. :. +// +// This means that it is possible to change the separator in +// such a way that makes some words unsearchable using a search +// string. +lunr.QueryLexer.termSeparator = lunr.tokenizer.separator + +lunr.QueryLexer.lexText = function (lexer) { + while (true) { + var char = lexer.next() + + if (char == lunr.QueryLexer.EOS) { + return lunr.QueryLexer.lexEOS + } + + // Escape character is '\' + if (char.charCodeAt(0) == 92) { + lexer.escapeCharacter() + continue + } + + if (char == ":") { + return lunr.QueryLexer.lexField + } + + if (char == "~") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexEditDistance + } + + if (char == "^") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexBoost + } + + // "+" indicates term presence is required + // checking for length to ensure that only + // leading "+" are considered + if (char == "+" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + // "-" indicates term presence is prohibited + // checking for length to ensure that only + // leading "-" are considered + if (char == "-" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + if (char.match(lunr.QueryLexer.termSeparator)) { + return lunr.QueryLexer.lexTerm + } + } +} + +lunr.QueryParser = function (str, query) { + this.lexer = new lunr.QueryLexer (str) + this.query = query + this.currentClause = {} + this.lexemeIdx = 0 +} + +lunr.QueryParser.prototype.parse = function () { + this.lexer.run() + this.lexemes = this.lexer.lexemes + + var state = lunr.QueryParser.parseClause + + while (state) { + state = state(this) + } + + return this.query +} + +lunr.QueryParser.prototype.peekLexeme = function () { + return this.lexemes[this.lexemeIdx] +} + +lunr.QueryParser.prototype.consumeLexeme = function () { + var lexeme = this.peekLexeme() + this.lexemeIdx += 1 + return lexeme +} + +lunr.QueryParser.prototype.nextClause = function () { + var completedClause = this.currentClause + this.query.clause(completedClause) + this.currentClause = {} +} + +lunr.QueryParser.parseClause = function (parser) { + var lexeme = parser.peekLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.type) { + case lunr.QueryLexer.PRESENCE: + return lunr.QueryParser.parsePresence + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expected either a field or a term, found " + lexeme.type + + if (lexeme.str.length >= 1) { + errorMessage += " with value '" + lexeme.str + "'" + } + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } +} + +lunr.QueryParser.parsePresence = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.str) { + case "-": + parser.currentClause.presence = lunr.Query.presence.PROHIBITED + break + case "+": + parser.currentClause.presence = lunr.Query.presence.REQUIRED + break + default: + var errorMessage = "unrecognised presence operator'" + lexeme.str + "'" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term or field, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseField = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + if (parser.query.allFields.indexOf(lexeme.str) == -1) { + var possibleFields = parser.query.allFields.map(function (f) { return "'" + f + "'" }).join(', '), + errorMessage = "unrecognised field '" + lexeme.str + "', possible fields: " + possibleFields + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.fields = [lexeme.str] + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseTerm = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + parser.currentClause.term = lexeme.str.toLowerCase() + + if (lexeme.str.indexOf("*") != -1) { + parser.currentClause.usePipeline = false + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseEditDistance = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var editDistance = parseInt(lexeme.str, 10) + + if (isNaN(editDistance)) { + var errorMessage = "edit distance must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.editDistance = editDistance + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseBoost = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var boost = parseInt(lexeme.str, 10) + + if (isNaN(boost)) { + var errorMessage = "boost must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.boost = boost + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like enviroments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})(); diff --git a/search/main.js b/search/main.js new file mode 100644 index 00000000..c5ccfa61 --- /dev/null +++ b/search/main.js @@ -0,0 +1,102 @@ +function getSearchTermFromLocation() { + var sPageURL = window.location.search.substring(1); + var sURLVariables = sPageURL.split('&'); + for (var i = 0; i < sURLVariables.length; i++) { + var sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] == 'q') { + return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); + } + } +} + +function joinUrl (base, path) { + if (path.substring(0, 1) === "/") { + // path starts with `/`. Thus it is absolute. + return path; + } + if (base.substring(base.length-1) === "/") { + // base ends with `/` + return base + path; + } + return base + "/" + path; +} + +function formatResult (location, title, summary) { + return ''; +} + +function displayResults (results) { + var search_results = document.getElementById("mkdocs-search-results"); + while (search_results.firstChild) { + search_results.removeChild(search_results.firstChild); + } + if (results.length > 0){ + for (var i=0; i < results.length; i++){ + var result = results[i]; + var html = formatResult(result.location, result.title, result.summary); + search_results.insertAdjacentHTML('beforeend', html); + } + } else { + var noResultsText = search_results.getAttribute('data-no-results-text'); + if (!noResultsText) { + noResultsText = "No results found"; + } + search_results.insertAdjacentHTML('beforeend', '

' + noResultsText + '

'); + } +} + +function doSearch () { + var query = document.getElementById('mkdocs-search-query').value; + if (query.length > min_search_length) { + if (!window.Worker) { + displayResults(search(query)); + } else { + searchWorker.postMessage({query: query}); + } + } else { + // Clear results for short queries + displayResults([]); + } +} + +function initSearch () { + var search_input = document.getElementById('mkdocs-search-query'); + if (search_input) { + search_input.addEventListener("keyup", doSearch); + } + var term = getSearchTermFromLocation(); + if (term) { + search_input.value = term; + doSearch(); + } +} + +function onWorkerMessage (e) { + if (e.data.allowSearch) { + initSearch(); + } else if (e.data.results) { + var results = e.data.results; + displayResults(results); + } else if (e.data.config) { + min_search_length = e.data.config.min_search_length-1; + } +} + +if (!window.Worker) { + console.log('Web Worker API not supported'); + // load index in main thread + $.getScript(joinUrl(base_url, "search/worker.js")).done(function () { + console.log('Loaded worker'); + init(); + window.postMessage = function (msg) { + onWorkerMessage({data: msg}); + }; + }).fail(function (jqxhr, settings, exception) { + console.error('Could not load worker.js'); + }); +} else { + // Wrap search in a web worker + var searchWorker = new Worker(joinUrl(base_url, "search/worker.js")); + searchWorker.postMessage({init: true}); + searchWorker.onmessage = onWorkerMessage; +} diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 00000000..8ef0477d --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"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. Setup tutorial IRC channel Mailing list","title":"Home"},{"location":"docker/","text":"Docker Official Docker image is available from Docker Hub. It expects configuration file to be available at /data/maddy.conf. If /data is a Docker volume, then default configuration will be placed there automatically. If it is used, then MADDY_HOSTNAME, MADDY_DOMAIN environment variables control the host name and primary domain for the server. TLS certificate should be placed in /data/tls/fullchain.pem, private key in /data/tls/privkey.pem DKIM keys are generated in /data/dkim_keys directory. Image tags latest - A latest stable release. May contain breaking changes. X.Y - A specific feature branch, it is recommended to use these tags to receive bugfixes without the risk of feature-related regressions or breaking changes. X.Y.Z - A specific stable release Ports All standard ports, as described in maddy docs. 25 - SMTP inbound port. 465 , 587 - SMTP Submission ports 993 , 143 - IMAP4 ports Volumes /data - maddy state directory. Databases, queues, etc are stored here. You might want to mount a named volume there. The main configuration file is stored here too ( /data/maddy.conf ). Management utility To run management commands, create a temporary container with the same /data directory and put the command after the image name, like this: docker run --rm -it -v maddydata:/data foxcpp/maddy:0.6.0 creds create foxcpp@maddy.test docker run --rm -it -v maddydata:/data foxcpp/maddy:0.6.0 imap-acct create foxcpp@maddy.test 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 foxcpp@maddy.test 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 587:587 \\ -p 993:993 \\ foxcpp/maddy:0.6 It will fail on first startup. Copy TLS certificate to /data/tls/fullchain.pem and key to /data/tls/privkey.pem. Run the server again. Finish DNS configuration (DKIM keys, etc) as described in tutorials/setting-up/ .","title":"Docker"},{"location":"docker/#docker","text":"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.","title":"Docker"},{"location":"docker/#image-tags","text":"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","title":"Image tags"},{"location":"docker/#ports","text":"All standard ports, as described in maddy docs. 25 - SMTP inbound port. 465 , 587 - SMTP Submission ports 993 , 143 - IMAP4 ports","title":"Ports"},{"location":"docker/#volumes","text":"/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 ).","title":"Volumes"},{"location":"docker/#management-utility","text":"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.6.0 creds create foxcpp@maddy.test docker run --rm -it -v maddydata:/data foxcpp/maddy:0.6.0 imap-acct create foxcpp@maddy.test 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 foxcpp@maddy.test","title":"Management utility"},{"location":"docker/#tldr","text":"docker volume create maddydata docker run \\ --name maddy \\ -e MADDY_HOSTNAME=mx.maddy.test \\ -e MADDY_DOMAIN=maddy.test \\ -v maddydata:/data \\ -p 25:25 \\ -p 143:143 \\ -p 587:587 \\ -p 993:993 \\ foxcpp/maddy:0.6 It will fail on first startup. Copy TLS certificate to /data/tls/fullchain.pem and key to /data/tls/privkey.pem. Run the server again. Finish DNS configuration (DKIM keys, etc) as described in tutorials/setting-up/ .","title":"TL;DR"},{"location":"faq/","text":"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 add rspamd line to checks in local_routing , it should just work in most cases. Is it production-ready? maddy is considered \"beta\" quality. Several people use it for personal email. Single process makes it unreliable. This is dumb! This is a compromise between ease of management and reliability. Several measures are implemented in code base in attempt to reduce possible effect of bugs in one component. Besides, you are not required to use a single process, it is easy to launch maddy with a non-default configuration path and connect multiple instances together using off-the-shelf protocols.","title":"Frequently Asked Questions"},{"location":"faq/#frequently-asked-questions","text":"","title":"Frequently Asked Questions"},{"location":"faq/#i-configured-maddy-as-recommended-and-gmail-still-puts-my-messages-in-spam","text":"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.","title":"I configured maddy as recommended and gmail still puts my messages in spam"},{"location":"faq/#message-sending-fails-with-dial-tcp-xxxx25-connect-connection-timed-out-in-log","text":"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\".","title":"Message sending fails with dial tcp X.X.X.X:25: connect: connection timed out in log"},{"location":"faq/#what-is-resource-usage-of-maddy","text":"For a small personal server, you do not need much more than a single 1 GiB of RAM and disk space.","title":"What is resource usage of maddy?"},{"location":"faq/#how-to-setup-a-catchall-address","text":"https://github.com/foxcpp/maddy/issues/243#issuecomment-655694512","title":"How to setup a catchall address?"},{"location":"faq/#maddy-command-prints-a-permission-denied-error","text":"Run maddy command under the same user as maddy itself. E.g. sudo -u maddy maddy creds ...","title":"maddy command prints a \"permission denied\" error"},{"location":"faq/#how-maddy-compares-to-mailcow-or-mail-in-the-box","text":"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.","title":"How maddy compares to MailCow or Mail-In-The-Box?"},{"location":"faq/#how-maddy-imap-server-compares-to-wildduck","text":"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.","title":"How maddy IMAP server compares to WildDuck?"},{"location":"faq/#how-maddy-smtp-server-compares-to-zonemta","text":"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).","title":"How maddy SMTP server compares to ZoneMTA?"},{"location":"faq/#is-there-a-webmail","text":"No, at least currently. I suggest you to check out alps if you are fine with alpha-quality but extremely easy to deploy webmail.","title":"Is there a webmail?"},{"location":"faq/#is-there-a-content-filter-spam-filter","text":"No. maddy moves email messages around, it does not classify them as bad or good with the notable exception of sender policies. It is possible to integrate rspamd using 'rspamd' module. Just add rspamd line to checks in local_routing , it should just work in most cases.","title":"Is there a content filter (spam filter)?"},{"location":"faq/#is-it-production-ready","text":"maddy is considered \"beta\" quality. Several people use it for personal email.","title":"Is it production-ready?"},{"location":"faq/#single-process-makes-it-unreliable-this-is-dumb","text":"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.","title":"Single process makes it unreliable. This is dumb!"},{"location":"multiple-domains/","text":"Multiple domains configuration Separate account namespaces Given two domains, example.org and example.com. foo@example.org and foo@example.com are different and completely independent accounts. All changes needed to make it work is to make sure all domains are specified in the $(local_domains) macro in the main configuration file. Note that you need to pick one domain as a \"primary\" for use in auto-generated messages. $(primary_domain) = example.org $(local_domains) = $(primary_domain) example.com The base configuration is 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 . Single account namespace You can configure maddy to only use local part of the email as an account identifier instead of the complete email. This needs two changes to default configuration: storage.imapsql local_mailboxes { ... delivery_map email_localpart auth_normalize precis_casefold } This way, when authenticating as foxcpp , it will be mapped to foxcpp storage account. E.g. you will need to run maddy imap-accts create foxcpp , without the domain part. If you have existing accounts, you will need to rename them. Change to auth_normalize is necessary so that normalization function will not attempt to parse authentication identity as a email. When a email is received, delivery_map email_localpart will strip the domain part before looking up the account. That is, foxcpp@example.org will be become just foxcpp . You also need to make authorize_sender check (used in submission endpoint) accept non-email usernames: authorize_sender { ... auth_normalize precis_casefold user_to_email regexp \"(.*)\" \"$1@$(primary_domain)\" } Note that is would work only if clients use only one domain as sender ( $(primary_domain) ). If you want to allow sending from all domains, you need to remove authorize_sender check altogether since it is not currently supported. After that you can create accounts without specifying the domain part: maddy imap-acct create foxcpp maddy creds create foxcpp And authenticate using \"foxcpp\" in email clients. Messages for any foxcpp@* address with a domain in $(local_domains) will be delivered to that mailbox.","title":"Multiple domains configuration"},{"location":"multiple-domains/#multiple-domains-configuration","text":"","title":"Multiple domains configuration"},{"location":"multiple-domains/#separate-account-namespaces","text":"Given two domains, example.org and example.com. foo@example.org and foo@example.com are different and completely independent accounts. All changes needed to make it work is to make sure all domains are specified in the $(local_domains) macro in the main configuration file. Note that you need to pick one domain as a \"primary\" for use in auto-generated messages. $(primary_domain) = example.org $(local_domains) = $(primary_domain) example.com The base configuration is 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 .","title":"Separate account namespaces"},{"location":"multiple-domains/#single-account-namespace","text":"You can configure maddy to only use local part of the email as an account identifier instead of the complete email. This needs two changes to default configuration: storage.imapsql local_mailboxes { ... delivery_map email_localpart auth_normalize precis_casefold } This way, when authenticating as foxcpp , it will be mapped to foxcpp storage account. E.g. you will need to run maddy imap-accts create foxcpp , without the domain part. If you have existing accounts, you will need to rename them. Change to auth_normalize is necessary so that normalization function will not attempt to parse authentication identity as a email. When a email is received, delivery_map email_localpart will strip the domain part before looking up the account. That is, foxcpp@example.org will be become just foxcpp . You also need to make authorize_sender check (used in submission endpoint) accept non-email usernames: authorize_sender { ... auth_normalize precis_casefold user_to_email regexp \"(.*)\" \"$1@$(primary_domain)\" } Note that is would work only if clients use only one domain as sender ( $(primary_domain) ). If you want to allow sending from all domains, you need to remove authorize_sender check altogether since it is not currently supported. After that you can create accounts without specifying the domain part: maddy imap-acct create foxcpp maddy creds create foxcpp And authenticate using \"foxcpp\" in email clients. Messages for any foxcpp@* address with a domain in $(local_domains) will be delivered to that mailbox.","title":"Single account namespace"},{"location":"seclevels/","text":"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 estabilished 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 recepient domain (even one in testing mode). MX level: DNSSEC. MX record is signed. TLS level: None. Plaintext connection was estabilished, TLS is not available or failed. TLS level: Encrypted. TLS connection was estabilished, the server certificate failed X.509 and DANE verification. TLS level: Authenticated. TLS connection was estabilished, 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.","title":"Outbound delivery security"},{"location":"seclevels/#outbound-delivery-security","text":"maddy implements a number of schemes and protocols for discovery and enforcement of security features supported by the recipient MTA.","title":"Outbound delivery security"},{"location":"seclevels/#introduction-to-the-problems-of-secure-smtp","text":"Outbound delivery security involves two independent problems: MX record authentication TLS enforcement","title":"Introduction to the problems of secure SMTP"},{"location":"seclevels/#mx-record-authentication","text":"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.","title":"MX record authentication"},{"location":"seclevels/#tls-enforcement","text":"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.","title":"TLS enforcement"},{"location":"seclevels/#maddy-policy-details","text":"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 estabilished 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 recepient domain (even one in testing mode). MX level: DNSSEC. MX record is signed. TLS level: None. Plaintext connection was estabilished, TLS is not available or failed. TLS level: Encrypted. TLS connection was estabilished, the server certificate failed X.509 and DANE verification. TLS level: Authenticated. TLS connection was estabilished, 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.","title":"maddy policy details"},{"location":"seclevels/#maddy-security-policies","text":"See Remote MX delivery for description of configuration options available for each policy mechanism supported by maddy.","title":"maddy security policies"},{"location":"upgrading/","text":"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_NAME to add credentials to pass_table store. Start the server back. systemctl start maddy 0.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\"","title":"Upgrading from older maddy versions"},{"location":"upgrading/#upgrading-from-older-maddy-versions","text":"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.","title":"Upgrading from older maddy versions"},{"location":"upgrading/#incompatible-version-migration","text":"","title":"Incompatible version migration"},{"location":"upgrading/#02-03","text":"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_NAME to add credentials to pass_table store. Start the server back. systemctl start maddy","title":"0.2 -> 0.3"},{"location":"upgrading/#01-02","text":"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\"","title":"0.1 -> 0.2"},{"location":"internals/quirks/","text":"Implementation quirks This page documents unusual behavior of the maddy protocols implementations. Some of these problems break standards, some don't but still can hurt interoperability. SMTP for field is never included in the Received header field. This is allowed by RFC 2821 . IMAP sql \\Recent flag is not reset in all cases. This does not break RFC 3501 . Clients relying on it will work (much) less efficiently.","title":"Implementation quirks"},{"location":"internals/quirks/#implementation-quirks","text":"This page documents unusual behavior of the maddy protocols implementations. Some of these problems break standards, some don't but still can hurt interoperability.","title":"Implementation quirks"},{"location":"internals/quirks/#smtp","text":"for field is never included in the Received header field. This is allowed by RFC 2821 .","title":"SMTP"},{"location":"internals/quirks/#imap","text":"","title":"IMAP"},{"location":"internals/quirks/#sql","text":"\\Recent flag is not reset in all cases. This does not break RFC 3501 . Clients relying on it will work (much) less efficiently.","title":"sql"},{"location":"internals/specifications/","text":"Followed specifications This page lists Internet Standards and other specifications followed by maddy along with any known deviations. Message format RFC 2822 - Internet Message Format RFC 2045 - Multipurpose Internet Mail Extensions (MIME) (part 1) RFC 2046 - Multipurpose Internet Mail Extensions (MIME) (part 2) RFC 2047 - Multipurpose Internet Mail Extensions (MIME) (part 3) RFC 2048 - Multipurpose Internet Mail Extensions (MIME) (part 4) RFC 2049 - Multipurpose Internet Mail Extensions (MIME) (part 5) RFC 6532 - Internationalized Email Headers RFC 2183 - Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field IMAP RFC 3501 - Internet Message Access Protocol - Version 4rev1 Partial : \\Recent flag is not reset sometimes. RFC 2152 - UTF-7 Extensions RFC 2595 - Using TLS with IMAP, POP3 and ACAP RFC 7889 - The IMAP APPENDLIMIT Extension RFC 3348 - The Internet Message Action Protocol (IMAP4). Child Mailbox Extension RFC 6851 - Internet Message Access Protocol (IMAP) - MOVE Extension RFC 6154 - IMAP LIST Extension for Special-Use Mailboxes Partial : Only SPECIAL-USE capability. RFC 5255 - Internet Message Access Protocol Internationalization Partial : Only I18NLEVEL=1 capability. RFC 4978 - The IMAP COMPRESS Extension RFC 3691 - Internet Message Access Protocol (IMAP) UNSELECT command RFC 2177 - IMAP4 IDLE command RFC 7888 - IMAP4 Non-Synchronizing Literals LITERAL+ capability. RFC 4959 - IMAP Extension for Simple Authentication and Security Layer (SASL) Initial Client Response SMTP RFC 2033 - Local Mail Transfer Protocol RFC 5321 - Simple Mail Transfer Protocol RFC 6409 - Message Submission for Mail Extensions RFC 1870 - SMTP Service Extension for Message Size Declaration RFC 2920 - SMTP Service Extension for Command Pipelining Server support only, not used by SMTP client RFC 2034 - SMTP Service Extension for Returning Enhanced Error Codes RFC 3207 - SMTP Service Extension for Secure SMTP over Transport Layer Security RFC 4954 - SMTP Service Extension for Authentication RFC 6152 - SMTP Extension for 8-bit MIME RFC 6531 - SMTP Extension for Internationalized Email Misc RFC 6522 - The Multipart/Report Content Type for the Reporting of Mail System Administrative Messages RFC 3464 - An Extensible Message Format for Delivery Status Notifications RFC 6533 - Internationalized Delivery Status and Disposition Notifications Email security User authentication RFC 4422 - Simple Authentication and Security Layer (SASL) RFC 4616 - The PLAIN Simple Authentication and Security Layer (SASL) Mechanism Sender authentication RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures RFC 7001 - Message Header Field for Indicating Message Authentication Status RFC 7208 - Sender Policy Framework (SPF) for Authorizing Use of Domains in Email, Version 1 RFC 7372 - Email Authentication Status Codes RFC 7479 - Domain-based Message Authentication, Reporting, and Conformance (DMARC) Partial : No report generation. RFC 8301 - Cryptographic Algorithm and Key Usage Update to DomainKeys Identified Mail (DKIM) RFC 8463 - A New Cryptographic Signature Method for DomainKeys Identified Mail (DKIM) RFC 8616 - Email Authentication for Internationalized Mail Recipient authentication RFC 4033 - DNS Security Introduction and Requirements RFC 6698 - The DNS-Based Authentication of Named Entities (DANE) Transport Layer Security (TLS) Protocol: TLSA RFC 7672 - SMTP Security via Opportunistic DNS-Based Authentication of Named Entities (DANE) Transport Layer Security (TLS) RFC 8461 - SMTP MTA Strict Transport Security (MTA-STS) Unicode, encodings, internationalization RFC 3492 - Punycode: A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA) RFC 3629 - UTF-8, a transformation format of ISO 10646 RFC 5890 - Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework RFC 5891 - Internationalized Domain Names for Applications (IDNA): Protocol RFC 7616 - Preparation, Enforcement, and Comparison of Internationalized Strings Representing Usernames and Passwords RFC 8264 - PRECIS Framework: Preparation, Enforcement, and Comparison of Internationalized Strings in Application Protocols Unicode 11.0.0 UAX #15 - Unicode Normalization Forms There is a huge list of non-Unicode encodings supported by message parser used for IMAP static cache and search. See Unicode support page for details. Misc RFC 5782 - DNS Blacklists and Whitelists","title":"Followed specifications"},{"location":"internals/specifications/#followed-specifications","text":"This page lists Internet Standards and other specifications followed by maddy along with any known deviations.","title":"Followed specifications"},{"location":"internals/specifications/#message-format","text":"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","title":"Message format"},{"location":"internals/specifications/#imap","text":"RFC 3501 - Internet Message Access Protocol - Version 4rev1 Partial : \\Recent flag is not reset sometimes. RFC 2152 - UTF-7","title":"IMAP"},{"location":"internals/specifications/#extensions","text":"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","title":"Extensions"},{"location":"internals/specifications/#smtp","text":"RFC 2033 - Local Mail Transfer Protocol RFC 5321 - Simple Mail Transfer Protocol RFC 6409 - Message Submission for Mail","title":"SMTP"},{"location":"internals/specifications/#extensions_1","text":"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","title":"Extensions"},{"location":"internals/specifications/#misc","text":"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","title":"Misc"},{"location":"internals/specifications/#email-security","text":"","title":"Email security"},{"location":"internals/specifications/#user-authentication","text":"RFC 4422 - Simple Authentication and Security Layer (SASL) RFC 4616 - The PLAIN Simple Authentication and Security Layer (SASL) Mechanism","title":"User authentication"},{"location":"internals/specifications/#sender-authentication","text":"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","title":"Sender authentication"},{"location":"internals/specifications/#recipient-authentication","text":"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)","title":"Recipient authentication"},{"location":"internals/specifications/#unicode-encodings-internationalization","text":"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.","title":"Unicode, encodings, internationalization"},{"location":"internals/specifications/#misc_1","text":"RFC 5782 - DNS Blacklists and Whitelists","title":"Misc"},{"location":"internals/sqlite/","text":"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.db It will take some time to complete, you can close the utility when the sqlite> prompt appears.","title":"maddy & SQLite"},{"location":"internals/sqlite/#maddy-sqlite","text":"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.","title":"maddy & SQLite"},{"location":"internals/sqlite/#wal-mode","text":"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.","title":"WAL mode"},{"location":"internals/sqlite/#query-planner-statistics","text":"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\".","title":"Query planner statistics"},{"location":"internals/sqlite/#auto-vacuum","text":"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).","title":"Auto-vacuum"},{"location":"internals/sqlite/#manual-vacuuming","text":"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.db It will take some time to complete, you can close the utility when the sqlite> prompt appears.","title":"Manual vacuuming"},{"location":"internals/unicode/","text":"Unicode support maddy has the first-class Unicode support in all components (modules). You do 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 messsages and process them locally.","title":"Unicode support"},{"location":"internals/unicode/#unicode-support","text":"maddy has the first-class Unicode support in all components (modules). You do 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.","title":"Unicode support"},{"location":"internals/unicode/#non-ascii-data-in-message-headers-and-bodies","text":"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.","title":"Non-ASCII data in message headers and bodies"},{"location":"internals/unicode/#configuration-files","text":"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.","title":"Configuration files"},{"location":"internals/unicode/#local-credentials","text":"'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.","title":"Local credentials"},{"location":"internals/unicode/#protocol-support","text":"","title":"Protocol support"},{"location":"internals/unicode/#smtputf8-extension","text":"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.","title":"SMTPUTF8 extension"},{"location":"internals/unicode/#imap-utf8-i18nlevel-extensions","text":"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 messsages and process them locally.","title":"IMAP UTF8, I18NLEVEL extensions"},{"location":"man/","text":"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.5 build.sh script in the repo root compiles and installs man pages if the scdoc utility is installed in the system.","title":"Index"},{"location":"man/#maddy-manual-pages","text":"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.5 build.sh script in the repo root compiles and installs man pages if the scdoc utility is installed in the system.","title":"maddy manual pages"},{"location":"man/_generated_maddy.1/","text":"Command line arguments Name maddy - Composable all-in-one mail server. Synopsis maddy [options...] Description Maddy is Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission Agent (MSA), IMAP server and a set of other essential protocols/schemes necessary to run secure email server implemented in one executable. Command line arguments -h, -help Show help message and exit. -config _path_ Path to the configuration file. Default is /etc/maddy/maddy.conf. -libexec _path_ Path to the libexec directory. Helper executables will be searched here. Default is /usr/lib/maddy. -log _targets..._ Comma-separated list of logging targets. Valid values are the same as the 'log' config directive. Affects logging before configuration parsing completes and after it, if the different value is not specified in the configuration. -debug Enable debug log. You want to use it when reporting bugs. -v Print version & build metadata.","title":"Command line arguments"},{"location":"man/_generated_maddy.1/#command-line-arguments","text":"","title":"Command line arguments"},{"location":"man/_generated_maddy.1/#name","text":"maddy - Composable all-in-one mail server.","title":"Name"},{"location":"man/_generated_maddy.1/#synopsis","text":"maddy [options...]","title":"Synopsis"},{"location":"man/_generated_maddy.1/#description","text":"Maddy is Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission Agent (MSA), IMAP server and a set of other essential protocols/schemes necessary to run secure email server implemented in one executable.","title":"Description"},{"location":"man/_generated_maddy.1/#command-line-arguments_1","text":"-h, -help Show help message and exit. -config _path_ Path to the configuration file. Default is /etc/maddy/maddy.conf. -libexec _path_ Path to the libexec directory. Helper executables will be searched here. Default is /usr/lib/maddy. -log _targets..._ Comma-separated list of logging targets. Valid values are the same as the 'log' config directive. Affects logging before configuration parsing completes and after it, if the different value is not specified in the configuration. -debug Enable debug log. You want to use it when reporting bugs. -v Print version & build metadata.","title":"Command line arguments"},{"location":"reference/config-syntax/","text":"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 arg1 Any line starting with # is ignored. Empty lines are ignored too. Quoting Strings with whitespace should be wrapped into double quotes to make sure they will be interpreted as a single argument. 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 above Environment variables Environment variables can be referenced in the configuration using either {env:VARIABLENAME} syntax. Non-existent variables are expanded to empty strings and not removed from the arguments list. In the following example, directive0 will have one argument independently of whether VAR is defined. 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 snippetname The above example will be expanded into the following configuration: unrelated0 unrelated1 a b c Import statement also can be used to include content from other files. It works exactly the same way as with snippets but the file path should be used instead. The path can be either relative to the location of the currently processed configuration file or absolute. If there are both snippet and file with the same name - snippet will be used. # /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 0 Data size values Similar to duration values, but fractions are not allowed and suffixes are different. Valid unit suffixes: \"G\" (gibibyte, 1024^3 bytes), \"M\" (mebibyte, 1024^2 bytes), \"K\" (kibibyte, 1024 bytes), \"B\" or \"b\" (byte). Examples: 32M 3M 5K 5b Also note that the following is not valid, unlike Duration values syntax: 32M5K ADDRESS DEFINITIONS Maddy configuration uses URL-like syntax to specify network addresses. unix://file_path Unix domain socket. Relative paths are relative to runtime directory (/run/maddy). tcp://ADDRESS:PORT TCP/IP socket. tls://ADDRESS:PORT TCP/IP socket using TLS. DUMMY MODULE No-op module. It doesn't need to be configured explicitly and can be referenced using \"dummy\" name. It can act as a delivery target or auth. provider. In the latter case, it will accept any credentials, allowing any client to authenticate using any username and password (use with care!).","title":"Configuration files syntax"},{"location":"reference/config-syntax/#configuration-files-syntax","text":"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 arg1 Any line starting with # is ignored. Empty lines are ignored too.","title":"Configuration files syntax"},{"location":"reference/config-syntax/#quoting","text":"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.","title":"Quoting"},{"location":"reference/config-syntax/#blocks","text":"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 above","title":"Blocks"},{"location":"reference/config-syntax/#environment-variables","text":"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.","title":"Environment variables"},{"location":"reference/config-syntax/#snippets-imports","text":"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 snippetname The above example will be expanded into the following configuration: unrelated0 unrelated1 a b c Import statement also can be used to include content from other files. It works exactly the same way as with snippets but the file path should be used instead. The path can be either relative to the location of the currently processed configuration file or absolute. If there are both snippet and file with the same name - snippet will be used. # /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.","title":"Snippets & imports"},{"location":"reference/config-syntax/#duration-values","text":"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 0","title":"Duration values"},{"location":"reference/config-syntax/#data-size-values","text":"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 5b Also note that the following is not valid, unlike Duration values syntax: 32M5K","title":"Data size values"},{"location":"reference/config-syntax/#address-definitions","text":"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.","title":"ADDRESS DEFINITIONS"},{"location":"reference/config-syntax/#dummy-module","text":"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!).","title":"DUMMY MODULE"},{"location":"reference/global-config/","text":"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). Syntax : 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. Syntax : runtime_dir path Default : /run/maddy The path to the runtime directory. Used for Unix sockets and other temporary objects. Should be writable. Syntax : 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. Syntax : autogenerated_msg_domain domain Default : not specified Domain that is used in From field for auto-generated messages (such as Delivery Status Notifications). Syntax : tls file cert_file pkey_file tls module reference tls 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. Syntax : 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. Syntax : log targets... log 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.log Note: Maddy does not perform log files rotation, this is the job of the logrotate daemon. Send SIGUSR1 to maddy process to make it reopen log files. Syntax : debug boolean Default : no Enable verbose logging for all modules. You don't need that unless you are reporting a bug.","title":"Global configuration directives"},{"location":"reference/global-config/#global-configuration-directives","text":"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). Syntax : 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. Syntax : runtime_dir path Default : /run/maddy The path to the runtime directory. Used for Unix sockets and other temporary objects. Should be writable. Syntax : 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. Syntax : autogenerated_msg_domain domain Default : not specified Domain that is used in From field for auto-generated messages (such as Delivery Status Notifications). Syntax : tls file cert_file pkey_file tls module reference tls 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. Syntax : 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. Syntax : log targets... log 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.log Note: Maddy does not perform log files rotation, this is the job of the logrotate daemon. Send SIGUSR1 to maddy process to make it reopen log files. Syntax : debug boolean Default : no Enable verbose logging for all modules. You don't need that unless you are reporting a bug.","title":"Global configuration directives"},{"location":"reference/modules/","text":"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 }","title":"Modules introduction"},{"location":"reference/modules/#modules-introduction","text":"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 }","title":"Modules introduction"},{"location":"reference/smtp-pipeline/","text":"SMTP message routing (pipeline) Message pipeline 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). Assocating each message recipient with one or more delivery targets. Delivery target is a module that does final processing (delivery) of the message. Message handling flow is as follows: - Execute checks referenced in top-level 'check' blocks (if any) Execute modifiers referenced in top-level 'modify' blocks (if any) If there are 'source' blocks - select one that matches message sender (as specified in MAIL FROM). If there are no 'source' blocks - entire configuration is assumed to be the 'default_source' block. Execute checks referenced in 'check' blocks inside selected 'source' block (if any). Execute modifiers referenced in 'modify' blocks inside selected 'source' block (if any). Then, for each recipient: - Select 'destination' block that matches it. If there are no 'destination' blocks - entire used 'source' block is interpreted as if it was a 'default_destination' block. Execute checks referenced in 'check' block inside selected 'destination' block (if any). Execute modifiers referenced in 'modify' block inside selected 'destination' block (if any). If used block contains 'reject' directive - reject the recipient with specified SMTP status code. If used block contains 'deliver_to' directive - pass the message to the specified target module. Only recipients that are handled by used block are visible to the target. Each recipient is handled only by a single 'destination' block, in case of overlapping 'destination' - 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 or 'reject' directive. If 'destination' blocks are used, then 'default_destination' block should also be used to specify behavior for unmatched recipients. Same goes for source blocks, 'default_source' should be used if 'source' is used. That is, pipeline configuration should explicitly specify behavior for each possible sender/recipient combination. Additionally, directives that specify final handling decision ('deliver_to', 'reject') can't be used at the same level as source/destination rules. Consider example: destination example.org { deliver_to local_mboxes } reject It is not obvious whether 'reject' applies to all recipients or just for non-example.org ones, hence this is not allowed. Complete configuration example using all of the mentioned directives: 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 Syntax : 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 } Syntax : 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 } Syntax : reject smtp_code smtp_enhanced_code error_description reject smtp_code smtp_enhanced_code reject smtp_code reject Context : destination block Messages handled by the configuration block with this directive will be rejected with the specified SMTP error. If you aren't sure which codes to use, use 541 and 5.4.0 with your message or just leave all arguments out, the error description will say \"message is rejected due to policy reasons\" which is usually what you want to mean. 'reject' can't be used in the same block with 'deliver_to' or 'destination/source' directives. Example: reject 541 5.4.0 \"We don't like example.org, go away\" Syntax : 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. Syntax : 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. Syntax : 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\" } Syntax : 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. Syntax : 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 } } Syntax : 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_routing","title":"SMTP message routing (pipeline)"},{"location":"reference/smtp-pipeline/#smtp-message-routing-pipeline","text":"","title":"SMTP message routing (pipeline)"},{"location":"reference/smtp-pipeline/#message-pipeline","text":"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). Assocating each message recipient with one or more delivery targets. Delivery target is a module that does final processing (delivery) of the message. Message handling flow is as follows: - Execute checks referenced in top-level 'check' blocks (if any) Execute modifiers referenced in top-level 'modify' blocks (if any) If there are 'source' blocks - select one that matches message sender (as specified in MAIL FROM). If there are no 'source' blocks - entire configuration is assumed to be the 'default_source' block. Execute checks referenced in 'check' blocks inside selected 'source' block (if any). Execute modifiers referenced in 'modify' blocks inside selected 'source' block (if any). Then, for each recipient: - Select 'destination' block that matches it. If there are no 'destination' blocks - entire used 'source' block is interpreted as if it was a 'default_destination' block. Execute checks referenced in 'check' block inside selected 'destination' block (if any). Execute modifiers referenced in 'modify' block inside selected 'destination' block (if any). If used block contains 'reject' directive - reject the recipient with specified SMTP status code. If used block contains 'deliver_to' directive - pass the message to the specified target module. Only recipients that are handled by used block are visible to the target. Each recipient is handled only by a single 'destination' block, in case of overlapping 'destination' - 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 or 'reject' directive. If 'destination' blocks are used, then 'default_destination' block should also be used to specify behavior for unmatched recipients. Same goes for source blocks, 'default_source' should be used if 'source' is used. That is, pipeline configuration should explicitly specify behavior for each possible sender/recipient combination. Additionally, directives that specify final handling decision ('deliver_to', 'reject') can't be used at the same level as source/destination rules. Consider example: destination example.org { deliver_to local_mboxes } reject It is not obvious whether 'reject' applies to all recipients or just for non-example.org ones, hence this is not allowed. Complete configuration example using all of the mentioned directives: 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 }","title":"Message pipeline"},{"location":"reference/smtp-pipeline/#directives","text":"Syntax : 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 } Syntax : 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 } Syntax : reject smtp_code smtp_enhanced_code error_description reject smtp_code smtp_enhanced_code reject smtp_code reject Context : destination block Messages handled by the configuration block with this directive will be rejected with the specified SMTP error. If you aren't sure which codes to use, use 541 and 5.4.0 with your message or just leave all arguments out, the error description will say \"message is rejected due to policy reasons\" which is usually what you want to mean. 'reject' can't be used in the same block with 'deliver_to' or 'destination/source' directives. Example: reject 541 5.4.0 \"We don't like example.org, go away\" Syntax : 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. Syntax : 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. Syntax : 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\" } Syntax : 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. Syntax : 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 } } Syntax : 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\" }","title":"Directives"},{"location":"reference/smtp-pipeline/#reusable-pipeline-snippets-msgpipeline-module","text":"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_routing","title":"Reusable pipeline snippets (msgpipeline module)"},{"location":"reference/tls-acme/","text":"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 put-your-email-here@example.org 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 &local_tls Currently the only supported challenge is dns-01 one therefore you also need to configure the DNS provider: tls.loader.acme local_tls { email maddy-acme@example.org 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 test@maddy.invalid agreed off challenge dns-01 dns ... } Syntax: debug boolean Default: global directive value Enable debug logging. Syntax: hostname str Default: global directive value Domain name to issue certificate for. Required. Syntax: store_path path Default: state_dir/acme Where to store issued certificates and associated metadata. Currently only filesystem-based store is supported. Syntax: ca url Default: Let's Encrypt production CA URL of ACME directory to use. Syntax: 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. Syntax: email str Default: not set Email to pass while registering an ACME account. Syntax: agreed boolean Default: false Whether you agreed to ToS of the CA service you are using. Syntax: 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 \"...\" }","title":"Automatic certificate management via ACME"},{"location":"reference/tls-acme/#automatic-certificate-management-via-acme","text":"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 put-your-email-here@example.org 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 &local_tls Currently the only supported challenge is dns-01 one therefore you also need to configure the DNS provider: tls.loader.acme local_tls { email maddy-acme@example.org agreed challenge dns-01 dns PROVIDER_NAME { ... } } See below for supported providers and necessary configuration for each.","title":"Automatic certificate management via ACME"},{"location":"reference/tls-acme/#configuration-directives","text":"tls.loader.acme { debug off hostname example.maddy.invalid store_path /var/lib/maddy/acme ca https://acme-v02.api.letsencrypt.org/directory test_ca https://acme-staging-v02.api.letsencrypt.org/directory email test@maddy.invalid agreed off challenge dns-01 dns ... } Syntax: debug boolean Default: global directive value Enable debug logging. Syntax: hostname str Default: global directive value Domain name to issue certificate for. Required. Syntax: store_path path Default: state_dir/acme Where to store issued certificates and associated metadata. Currently only filesystem-based store is supported. Syntax: ca url Default: Let's Encrypt production CA URL of ACME directory to use. Syntax: 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. Syntax: email str Default: not set Email to pass while registering an ACME account. Syntax: agreed boolean Default: false Whether you agreed to ToS of the CA service you are using. Syntax: challenge dns-01 Default: not set Challenge(s) to use while performing domain verification.","title":"Configuration directives"},{"location":"reference/tls-acme/#dns-providers","text":"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 \"...\" }","title":"DNS providers"},{"location":"reference/tls/","text":"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. Syntax : protocols min_version max_version protocols 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 Syntax : ciphers ciphers... Default : Go version-defined set of 'secure ciphers', ordered by hardware performance List of supported cipher suites, in preference order. Not used with TLS 1.3. Valid values: RSA-WITH-RC4128-SHA RSA-WITH-3DES-EDE-CBC-SHA RSA-WITH-AES128-CBC-SHA RSA-WITH-AES256-CBC-SHA RSA-WITH-AES128-CBC-SHA256 RSA-WITH-AES128-GCM-SHA256 RSA-WITH-AES256-GCM-SHA384 ECDHE-ECDSA-WITH-RC4128-SHA ECDHE-ECDSA-WITH-AES128-CBC-SHA ECDHE-ECDSA-WITH-AES256-CBC-SHA ECDHE-RSA-WITH-RC4128-SHA ECDHE-RSA-WITH-3DES-EDE-CBC-SHA ECDHE-RSA-WITH-AES128-CBC-SHA ECDHE-RSA-WITH-AES256-CBC-SHA ECDHE-ECDSA-WITH-AES128-CBC-SHA256 ECDHE-RSA-WITH-AES128-CBC-SHA256 ECDHE-RSA-WITH-AES128-GCM-SHA256 ECDHE-ECDSA-WITH-AES128-GCM-SHA256 ECDHE-RSA-WITH-AES256-GCM-SHA384 ECDHE-ECDSA-WITH-AES256-GCM-SHA384 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-WITH-CHACHA20-POLY1305 Syntax : 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 } Syntax : protocols min_version max_version protocols 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 Syntax : 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. Syntax : 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. Syntax : root_ca paths... Default : system CA pool List of files with PEM-encoded CA certificates to use when verifying server certificates. Syntax : 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.","title":"TLS configuration"},{"location":"reference/tls/#tls-configuration","text":"","title":"TLS configuration"},{"location":"reference/tls/#server-side","text":"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 ... }","title":"Server-side"},{"location":"reference/tls/#available-certificate-loaders","text":"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).","title":"Available certificate loaders"},{"location":"reference/tls/#advanced-tls-configuration","text":"Note: maddy uses secure defaults and TLS handshake is resistant to active downgrade attacks. There is no need to change anything in most cases. Syntax : protocols min_version max_version protocols 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 Syntax : ciphers ciphers... Default : Go version-defined set of 'secure ciphers', ordered by hardware performance List of supported cipher suites, in preference order. Not used with TLS 1.3. Valid values: RSA-WITH-RC4128-SHA RSA-WITH-3DES-EDE-CBC-SHA RSA-WITH-AES128-CBC-SHA RSA-WITH-AES256-CBC-SHA RSA-WITH-AES128-CBC-SHA256 RSA-WITH-AES128-GCM-SHA256 RSA-WITH-AES256-GCM-SHA384 ECDHE-ECDSA-WITH-RC4128-SHA ECDHE-ECDSA-WITH-AES128-CBC-SHA ECDHE-ECDSA-WITH-AES256-CBC-SHA ECDHE-RSA-WITH-RC4128-SHA ECDHE-RSA-WITH-3DES-EDE-CBC-SHA ECDHE-RSA-WITH-AES128-CBC-SHA ECDHE-RSA-WITH-AES256-CBC-SHA ECDHE-ECDSA-WITH-AES128-CBC-SHA256 ECDHE-RSA-WITH-AES128-CBC-SHA256 ECDHE-RSA-WITH-AES128-GCM-SHA256 ECDHE-ECDSA-WITH-AES128-GCM-SHA256 ECDHE-RSA-WITH-AES256-GCM-SHA384 ECDHE-ECDSA-WITH-AES256-GCM-SHA384 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-WITH-CHACHA20-POLY1305 Syntax : 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.","title":"Advanced TLS configuration"},{"location":"reference/tls/#client","text":"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 } Syntax : protocols min_version max_version protocols 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 Syntax : 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. Syntax : 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. Syntax : root_ca paths... Default : system CA pool List of files with PEM-encoded CA certificates to use when verifying server certificates. Syntax : 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.","title":"Client"},{"location":"reference/auth/dovecot_sasl/","text":"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_path Configuration directives Syntax : 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.","title":"Dovecot SASL"},{"location":"reference/auth/dovecot_sasl/#dovecot-sasl","text":"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_path","title":"Dovecot SASL"},{"location":"reference/auth/dovecot_sasl/#configuration-directives","text":"Syntax : 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.","title":"Configuration directives"},{"location":"reference/auth/external/","text":"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 Syntax : helper _file_path_ Location of the helper binary. Required. Syntax : 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. Syntax : 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.","title":"System command"},{"location":"reference/auth/external/#system-command","text":"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 }","title":"System command"},{"location":"reference/auth/external/#configuration-directives","text":"Syntax : helper _file_path_ Location of the helper binary. Required. Syntax : 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. Syntax : 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.","title":"Configuration directives"},{"location":"reference/auth/ldap/","text":"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 Syntax: 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. Syntax: bind off bind unauth bind external bind 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. Syntax: dn_template _template_ DN template to use for binding. '{username}' is replaced with the username specified by the user. Syntax: base_dn _dn_ Base DN to use for lookup. Syntax: 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})) Syntax: starttls bool Default: off Whether to upgrade connection to TLS using STARTTLS. Syntax: tls_client { ... } Advanced TLS client configuration. See TLS configuration / Client for details. Syntax: connect_timeout duration Default: 1m Timeout for initial connection to the directory server. Syntax: request_timeout duration Default: 1m Timeout for each request (binding, lookup).","title":"LDAP BindDN"},{"location":"reference/auth/ldap/#ldap-binddn","text":"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 { ... }","title":"LDAP BindDN"},{"location":"reference/auth/ldap/#configuration-directives","text":"Syntax: 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. Syntax: bind off bind unauth bind external bind 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. Syntax: dn_template _template_ DN template to use for binding. '{username}' is replaced with the username specified by the user. Syntax: base_dn _dn_ Base DN to use for lookup. Syntax: 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})) Syntax: starttls bool Default: off Whether to upgrade connection to TLS using STARTTLS. Syntax: tls_client { ... } Advanced TLS client configuration. See TLS configuration / Client for details. Syntax: connect_timeout duration Default: 1m Timeout for initial connection to the directory server. Syntax: request_timeout duration Default: 1m Timeout for each request (binding, lookup).","title":"Configuration directives"},{"location":"reference/auth/pam/","text":"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 Syntax : debug boolean Default : no Enable verbose logging for all modules. You don't need that unless you are reporting a bug. Syntax : use_helper boolean Default : no Use LibexecDirectory/maddy-pam-helper instead of directly calling libpam. You need to use that if: 1. maddy is not compiled with libpam, but maddy-pam-helper is built separately. 2. maddy is running as an unprivileged user and used PAM configuration requires additional privileges (e.g. when using system accounts). For 2, you need to make maddy-pam-helper binary setuid, see README.md in source tree for details. 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-helper","title":"PAM"},{"location":"reference/auth/pam/#pam","text":"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 }","title":"PAM"},{"location":"reference/auth/pam/#configuration-directives","text":"Syntax : debug boolean Default : no Enable verbose logging for all modules. You don't need that unless you are reporting a bug. Syntax : use_helper boolean Default : no Use LibexecDirectory/maddy-pam-helper instead of directly calling libpam. You need to use that if: 1. maddy is not compiled with libpam, but maddy-pam-helper is built separately. 2. maddy is running as an unprivileged user and used PAM configuration requires additional privileges (e.g. when using system accounts). For 2, you need to make maddy-pam-helper binary setuid, see README.md in source tree for details. 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-helper","title":"Configuration directives"},{"location":"reference/auth/pass_table/","text":"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 } Shortened variant for inline use: pass_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.","title":"Password table"},{"location":"reference/auth/pass_table/#password-table","text":"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
} Shortened variant for inline use: pass_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 ... }","title":"Password table"},{"location":"reference/auth/pass_table/#password-hashes","text":"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.","title":"Password hashes"},{"location":"reference/auth/pass_table/#maddy-creds","text":"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.","title":"maddy creds"},{"location":"reference/auth/plain_separate/","text":"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 Syntax: user _table module_ Configuration block for any module from maddy-tables(5) can be used here. Example: user file /etc/maddy/allowed_users Syntax: pass _auth provider_ Configuration block for any auth. provider module can be used here, even 'plain_split' itself. The used auth. provider must provide username:password pair-based authentication.","title":"Separate username and password lookup"},{"location":"reference/auth/plain_separate/#separate-username-and-password-lookup","text":"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.","title":"Separate username and password lookup"},{"location":"reference/auth/plain_separate/#configuration-directives","text":"Syntax: user _table module_ Configuration block for any module from maddy-tables(5) can be used here. Example: user file /etc/maddy/allowed_users Syntax: pass _auth provider_ Configuration block for any auth. provider module can be used here, even 'plain_split' itself. The used auth. provider must provide username:password pair-based authentication.","title":"Configuration directives"},{"location":"reference/auth/shadow/","text":"/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 Syntax : debug boolean Default : no Enable verbose logging for all modules. You don't need that unless you are reporting a bug. Syntax : 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-helper","title":"/etc/shadow"},{"location":"reference/auth/shadow/#etcshadow","text":"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 }","title":"/etc/shadow"},{"location":"reference/auth/shadow/#configuration-directives","text":"Syntax : debug boolean Default : no Enable verbose logging for all modules. You don't need that unless you are reporting a bug. Syntax : 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-helper","title":"Configuration directives"},{"location":"reference/blob/fs/","text":"Filesystem This module stores message bodies in a file system directory. storage.blob.fs { root } storage.blob.fs Configuration directives Syntax: 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.","title":"Filesystem"},{"location":"reference/blob/fs/#filesystem","text":"This module stores message bodies in a file system directory. storage.blob.fs { root } storage.blob.fs ","title":"Filesystem"},{"location":"reference/blob/fs/#configuration-directives","text":"Syntax: 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.","title":"Configuration directives"},{"location":"reference/blob/s3/","text":"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 Syntax: endpoint _address:port_ REQUIRED. Root S3 endpoint. e.g. s3.amazonaws.com Syntax: secure boolean Default: yes Whether TLS should be used. Syntax: access_key string Syntax: secret_key _string_ REQUIRED. Static S3 credentials. Syntax: bucket _name_ REQUIRED. S3 bucket name. The bucket must exist and be read-writable. Syntax: region string Default: not set S3 bucket location. May be called \"endpoint\" in some manuals. Syntax: 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. Syntax: creds string Default: access_key Credentials to use for accessing the S3 Bucket. Credential Types: - access_key: use AWS access key and secret access key - file_minio: use credentials for Minio present at ~/.mc/config.json - file_aws: use credentials for AWS S3 present at ~/.aws/credentials - iam: use AWS IAM instance profile for credentials. By default, access_key is used with the access key and secret access key present in the config.","title":"Amazon S3"},{"location":"reference/blob/s3/#amazon-s3","text":"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 } }","title":"Amazon S3"},{"location":"reference/blob/s3/#configuration-directives","text":"Syntax: endpoint _address:port_ REQUIRED. Root S3 endpoint. e.g. s3.amazonaws.com Syntax: secure boolean Default: yes Whether TLS should be used. Syntax: access_key string Syntax: secret_key _string_ REQUIRED. Static S3 credentials. Syntax: bucket _name_ REQUIRED. S3 bucket name. The bucket must exist and be read-writable. Syntax: region string Default: not set S3 bucket location. May be called \"endpoint\" in some manuals. Syntax: 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. Syntax: creds string Default: access_key Credentials to use for accessing the S3 Bucket. Credential Types: - access_key: use AWS access key and secret access key - file_minio: use credentials for Minio present at ~/.mc/config.json - file_aws: use credentials for AWS S3 present at ~/.aws/credentials - iam: use AWS IAM instance profile for credentials. By default, access_key is used with the access key and secret access key present in the config.","title":"Configuration directives"},{"location":"reference/checks/actions/","text":"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.","title":"Check actions"},{"location":"reference/checks/actions/#check-actions","text":"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.","title":"Check actions"},{"location":"reference/checks/authorize_sender/","text":"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 precis_casefold_email from_normalize precis_casefold_email } check { authorize_sender { ... } } Configuration directives Syntax: user_to_email table Default: identity Table to use for lookups. Result of the lookup should contain either the domain name, the full email address or \" \" string. If it is just domain - user will be allowed to use any mailbox within a domain as a sender address. If result contains \" \" - user will be allowed to use any address. Syntax: 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. Syntax: unauth_action action Default: reject What to do if the user is not authenticated at all. Syntax: no_match_action action Default: reject What to do if user is not allowed to use the sender address specified. Syntax: malformed_action action Default: reject What to do if From or Sender header fields contain malformed values. Syntax: err_action action Default: reject What to do if error happens during prepare_email or user_to_email lookup. Syntax: auth_normalize action Default: precis_casefold_email Normalization function to apply to authorization username before further processing. Available options: - precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain - precis_casefold PRECIS UsernameCaseMapped profile for the entire string - precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain - precis PRECIS UsernameCasePreserved profile for the entire string - casefold Convert to lower case - noop Nothing Syntax: from_normalize action Default: precis_casefold_email Normalization function to apply to email addresses before further processing. Available options are same as for auth_normalize.","title":"MAIL FROM and From authorization"},{"location":"reference/checks/authorize_sender/#mail-from-and-from-authorization","text":"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 precis_casefold_email from_normalize precis_casefold_email } check { authorize_sender { ... } }","title":"MAIL FROM and From authorization"},{"location":"reference/checks/authorize_sender/#configuration-directives","text":"Syntax: user_to_email table Default: identity Table to use for lookups. Result of the lookup should contain either the domain name, the full email address or \" \" string. If it is just domain - user will be allowed to use any mailbox within a domain as a sender address. If result contains \" \" - user will be allowed to use any address. Syntax: 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. Syntax: unauth_action action Default: reject What to do if the user is not authenticated at all. Syntax: no_match_action action Default: reject What to do if user is not allowed to use the sender address specified. Syntax: malformed_action action Default: reject What to do if From or Sender header fields contain malformed values. Syntax: err_action action Default: reject What to do if error happens during prepare_email or user_to_email lookup. Syntax: auth_normalize action Default: precis_casefold_email Normalization function to apply to authorization username before further processing. Available options: - precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain - precis_casefold PRECIS UsernameCaseMapped profile for the entire string - precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain - precis PRECIS UsernameCasePreserved profile for the entire string - casefold Convert to lower case - noop Nothing Syntax: from_normalize action Default: precis_casefold_email Normalization function to apply to email addresses before further processing. Available options are same as for auth_normalize.","title":"Configuration directives"},{"location":"reference/checks/command/","text":"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 Syntax : 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}. Syntax : code integer ignore code integer quarantine code integer reject [SMTP code] [SMTP enhanced code] [SMTP message] This directives specified 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 action can be overriden using the 'code' directive.","title":"System command filter"},{"location":"reference/checks/command/#system-command-filter","text":"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 }","title":"System command filter"},{"location":"reference/checks/command/#arguments","text":"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.","title":"Arguments"},{"location":"reference/checks/command/#command-stdout","text":"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.","title":"Command stdout"},{"location":"reference/checks/command/#configuration-directives","text":"Syntax : 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}. Syntax : code integer ignore code integer quarantine code integer reject [SMTP code] [SMTP enhanced code] [SMTP message] This directives specified 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 action can be overriden using the 'code' directive.","title":"Configuration directives"},{"location":"reference/checks/dkim/","text":"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 } Syntax : debug boolean Default : global directive value Log both successfull and unsuccessful check executions instead of just unsuccessful. Syntax : 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. Syntax : 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. Syntax : 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. Syntax : 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.","title":"DKIM"},{"location":"reference/checks/dkim/#dkim","text":"This is the check module that performs verification of the DKIM signatures present on the incoming messages.","title":"DKIM"},{"location":"reference/checks/dkim/#configuration-directives","text":"check.dkim { debug no required_fields From Subject allow_body_subset no no_sig_action ignore broken_sig_action ignore fail_open no } Syntax : debug boolean Default : global directive value Log both successfull and unsuccessful check executions instead of just unsuccessful. Syntax : 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. Syntax : 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. Syntax : 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. Syntax : 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.","title":"Configuration directives"},{"location":"reference/checks/dnsbl/","text":"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 specifing 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 } } 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 Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : check_early boolean Default : no Check BLs before mail delivery starts and silently reject blacklisted clients. For this to work correctly, check should not be used in source/destination pipeline block. In particular, this means: - No logging is done for rejected messages. - No action is taken if quarantine_threshold is hit, only reject_threshold applies. - defer_sender_reject from SMTP configuration takes no effect. - MAIL FROM is not checked, even if specified. If you often get hit by spam attacks, it is recommended to enable this setting to save server resources. Syntax : quarantine_threshold integer Default : 1 DNSBL score needed (equals-or-higher) to quarantine the message. Syntax : 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. Syntax : client_ipv4 boolean Default : yes Whether to check address of the IPv4 clients against the list. Syntax : client_ipv6 boolean Default : yes Whether to check address of the IPv6 clients against the list. Syntax : 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. Syntax : 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. Syntax : 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. Syntax : 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.","title":"DNSBL lookup"},{"location":"reference/checks/dnsbl/#dnsbl-lookup","text":"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 specifing 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 } }","title":"DNSBL lookup"},{"location":"reference/checks/dnsbl/#arguments","text":"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 } } }","title":"Arguments"},{"location":"reference/checks/dnsbl/#configuration-directives","text":"Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : check_early boolean Default : no Check BLs before mail delivery starts and silently reject blacklisted clients. For this to work correctly, check should not be used in source/destination pipeline block. In particular, this means: - No logging is done for rejected messages. - No action is taken if quarantine_threshold is hit, only reject_threshold applies. - defer_sender_reject from SMTP configuration takes no effect. - MAIL FROM is not checked, even if specified. If you often get hit by spam attacks, it is recommended to enable this setting to save server resources. Syntax : quarantine_threshold integer Default : 1 DNSBL score needed (equals-or-higher) to quarantine the message. Syntax : reject_threshold integer Default : 9999 DNSBL score needed (equals-or-higher) to reject the message.","title":"Configuration directives"},{"location":"reference/checks/dnsbl/#list-configuration","text":"dnsbl.example.org dnsbl.example.com { client_ipv4 yes client_ipv6 no ehlo no mailfrom no responses 127.0.0.1/24 score 1 } Directive name and arguments specify the actual DNS zone to query when checking the list. Using multiple arguments is equivalent to specifying the same configuration separately for each list. Syntax : client_ipv4 boolean Default : yes Whether to check address of the IPv4 clients against the list. Syntax : client_ipv6 boolean Default : yes Whether to check address of the IPv6 clients against the list. Syntax : 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. Syntax : 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. Syntax : 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. Syntax : 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.","title":"List configuration"},{"location":"reference/checks/milter/","text":"Milter client The 'milter' implements subset of Sendmail's milter protocol that can be used to integrate external software with maddy. maddy implements version 6 of the protocol, older versions are not supported. Notable limitations of protocol implementation in maddy include: 1. Changes of envelope sender address are not supported 2. Removal and addition of envelope recipients is not supported 3. Removal and replacement of header fields is not supported 4. Headers fields can be inserted only on top 5. Milter does not receive some \"macros\" provided by sendmail. Restrictions 1 and 2 are inherent to the maddy checks interface and cannot be removed without major changes to it. Restrictions 3, 4 and 5 are temporary due to incomplete implementation. check.milter { endpoint fail_open false } milter Arguments When defined inline, the first argument specifies endpoint to access milter via. See below. Configuration directives Syntax: 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' Syntax: 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.","title":"Milter client"},{"location":"reference/checks/milter/#milter-client","text":"The 'milter' implements subset of Sendmail's milter protocol that can be used to integrate external software with maddy. maddy implements version 6 of the protocol, older versions are not supported. Notable limitations of protocol implementation in maddy include: 1. Changes of envelope sender address are not supported 2. Removal and addition of envelope recipients is not supported 3. Removal and replacement of header fields is not supported 4. Headers fields can be inserted only on top 5. Milter does not receive some \"macros\" provided by sendmail. Restrictions 1 and 2 are inherent to the maddy checks interface and cannot be removed without major changes to it. Restrictions 3, 4 and 5 are temporary due to incomplete implementation. check.milter { endpoint fail_open false } milter ","title":"Milter client"},{"location":"reference/checks/milter/#arguments","text":"When defined inline, the first argument specifies endpoint to access milter via. See below.","title":"Arguments"},{"location":"reference/checks/milter/#configuration-directives","text":"Syntax: 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' Syntax: 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.","title":"Configuration directives"},{"location":"reference/checks/misc/","text":"Misc checks Configuration directives Following directives are defined for all modules listed below. Syntax : fail_action ignore fail_action reject fail_action quarantine Default : quarantine Action to take when check fails. See Check actions for details. Syntax : debug boolean Default : global directive value Log both sucessfull and unsucessfull check executions instead of just unsucessfull. require_mx_record Check that domain in MAIL FROM command does have a MX record and none of them are \"null\" (contain a single dot as the host). By default, quarantines messages coming from servers missing MX records, use 'fail_action' directive to change that. require_matching_rdns Check that source server IP does have a PTR record point to the domain specified in EHLO/HELO command. By default, quarantines messages coming from servers with mismatched or missing PTR record, use 'fail_action' directive to change that. require_tls Check that the source server is connected via TLS; either directly, or by using the STARTTLS command. By default, rejects messages coming from unencrypted servers. Use the 'fail_action' directive to change that.","title":"Misc checks"},{"location":"reference/checks/misc/#misc-checks","text":"","title":"Misc checks"},{"location":"reference/checks/misc/#configuration-directives","text":"Following directives are defined for all modules listed below. Syntax : fail_action ignore fail_action reject fail_action quarantine Default : quarantine Action to take when check fails. See Check actions for details. Syntax : debug boolean Default : global directive value Log both sucessfull and unsucessfull check executions instead of just unsucessfull.","title":"Configuration directives"},{"location":"reference/checks/misc/#require_mx_record","text":"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.","title":"require_mx_record"},{"location":"reference/checks/misc/#require_matching_rdns","text":"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.","title":"require_matching_rdns"},{"location":"reference/checks/misc/#require_tls","text":"Check that the source server is connected via TLS; either directly, or by using the STARTTLS command. By default, rejects messages coming from unencrypted servers. Use the 'fail_action' directive to change that.","title":"require_tls"},{"location":"reference/checks/rspamd/","text":"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 flags pass_all } rspamd http://127.0.0.1:11333 Configuration directives Syntax: tls_client { ... } Default: not set Configure TLS client if HTTPS is used. See TLS configuration / Client for details. Syntax: 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. Syntax: settings_id string Default: not set Settings ID to pass to the server. Syntax: tag string Default: maddy Value to send in MTA-Tag header field. Syntax: hostname string Default: value of global directive Value to send in MTA-Name header field. Syntax: io_error_action action Default: ignore Action to take in case of inability to contact the rspamd server. Syntax: error_resp_action action Default: ignore Action to take in case of 5xx or 4xx response received from the rspamd server. Syntax: 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. Syntax: 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. Syntax: flags string list... Default: pass_all Flags to pass to the rspamd server. See https://rspamd.com/doc/architecture/protocol.html for details.","title":"rspamd"},{"location":"reference/checks/rspamd/#rspamd","text":"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 flags pass_all } rspamd http://127.0.0.1:11333","title":"rspamd"},{"location":"reference/checks/rspamd/#configuration-directives","text":"Syntax: tls_client { ... } Default: not set Configure TLS client if HTTPS is used. See TLS configuration / Client for details. Syntax: 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. Syntax: settings_id string Default: not set Settings ID to pass to the server. Syntax: tag string Default: maddy Value to send in MTA-Tag header field. Syntax: hostname string Default: value of global directive Value to send in MTA-Name header field. Syntax: io_error_action action Default: ignore Action to take in case of inability to contact the rspamd server. Syntax: error_resp_action action Default: ignore Action to take in case of 5xx or 4xx response received from the rspamd server. Syntax: 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. Syntax: 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. Syntax: flags string list... Default: pass_all Flags to pass to the rspamd server. See https://rspamd.com/doc/architecture/protocol.html for details.","title":"Configuration directives"},{"location":"reference/checks/spf/","text":"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 } Syntax : debug boolean Default : global directive value Enable verbose logging for check.spf. Syntax : 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). Syntax : none_action reject|qurantine|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. Syntax : neutral_action reject|qurantine|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. Syntax : fail_action reject|qurantine|ignore Default : quarantine Action to take when SPF policy evaluates to a 'fail' result. Syntax : softfail_action reject|qurantine|ignore Default : ignore Action to take when SPF policy evaluates to a 'softfail' result. Syntax : permerr_action reject|qurantine|ignore Default : reject Action to take when SPF policy evaluates to a 'permerror' result. Syntax : temperr_action reject|qurantine|ignore Default : reject Action to take when SPF policy evaluates to a 'temperror' result.","title":"SPF"},{"location":"reference/checks/spf/#spf","text":"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.","title":"SPF"},{"location":"reference/checks/spf/#dmarc-override","text":"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.","title":"DMARC override"},{"location":"reference/checks/spf/#configuration-directives","text":"check.spf { debug no enforce_early no fail_action quarantine softfail_action ignore permerr_action reject temperr_action reject } Syntax : debug boolean Default : global directive value Enable verbose logging for check.spf. Syntax : 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). Syntax : none_action reject|qurantine|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. Syntax : neutral_action reject|qurantine|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. Syntax : fail_action reject|qurantine|ignore Default : quarantine Action to take when SPF policy evaluates to a 'fail' result. Syntax : softfail_action reject|qurantine|ignore Default : ignore Action to take when SPF policy evaluates to a 'softfail' result. Syntax : permerr_action reject|qurantine|ignore Default : reject Action to take when SPF policy evaluates to a 'permerror' result. Syntax : temperr_action reject|qurantine|ignore Default : reject Action to take when SPF policy evaluates to a 'temperror' result.","title":"Configuration directives"},{"location":"reference/endpoints/imap/","text":"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 auth pam storage &local_mailboxes } Syntax : tls certificate_path key_path { ... } Default : global directive value TLS certificate & key to use. Fine-tuning of other TLS properties is possible by specifing a configuration block and options inside it: tls cert.crt key.key { protocols tls1.2 tls1.3 } See TLS configuration / Server for details. Syntax : io_debug boolean Default : no Write all commands and responses to stderr. Syntax : io_errors boolean Default : no Log I/O errors. Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : insecure_auth boolean Default : no (yes if TLS is disabled) Syntax : auth _module_reference_ Use the specified module for authentication. Required. Syntax : storage _module_reference_ Use the specified module for message storage. Required.","title":"IMAP4rev1 endpoint"},{"location":"reference/endpoints/imap/#imap4rev1-endpoint","text":"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.","title":"IMAP4rev1 endpoint"},{"location":"reference/endpoints/imap/#configuration-directives","text":"imap tcp://0.0.0.0:143 tls://0.0.0.0:993 { tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key io_debug no debug no insecure_auth no auth pam storage &local_mailboxes } Syntax : tls certificate_path key_path { ... } Default : global directive value TLS certificate & key to use. Fine-tuning of other TLS properties is possible by specifing a configuration block and options inside it: tls cert.crt key.key { protocols tls1.2 tls1.3 } See TLS configuration / Server for details. Syntax : io_debug boolean Default : no Write all commands and responses to stderr. Syntax : io_errors boolean Default : no Log I/O errors. Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : insecure_auth boolean Default : no (yes if TLS is disabled) Syntax : auth _module_reference_ Use the specified module for authentication. Required. Syntax : storage _module_reference_ Use the specified module for message storage. Required.","title":"Configuration directives"},{"location":"reference/endpoints/openmetrics/","text":"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}","title":"OpenMetrics/Prometheus telemetry"},{"location":"reference/endpoints/openmetrics/#openmetricsprometheus-telemetry","text":"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 .","title":"OpenMetrics/Prometheus telemetry"},{"location":"reference/endpoints/openmetrics/#metrics","text":"# AUTH command failures due to invalid credentials. maddy_smtp_failed_logins{module} # Failed SMTP transaction commands (MAIL, RCPT, DATA). maddy_smtp_failed_commands{module, command, smtp_code, smtp_enchcode} # Messages rejected with 4xx code due to ratelimiting. maddy_smtp_ratelimit_deferred{module} # Amount of started SMTP transactions started. maddy_smtp_started_transactions{module} # Amount of aborted SMTP transactions started. maddy_smtp_aborted_transactions{module} # Amount of completed SMTP transactions. maddy_smtp_completed_transactions{module} # Number of times a check returned 'reject' result (may be more than processed # messages if check does so on per-recipient basis). maddy_check_reject{check} # Number of times a check returned 'quarantine' result (may be more than # processed messages if check does so on per-recipient basis). maddy_check_quarantined{check} # Amount of queued messages. maddy_queue_length{module, location} # Outbound connections established with specific TLS security level. maddy_remote_conns_tls_level{module, level} # Outbound connections established with specific MX security level. maddy_remote_conns_mx_level{module, level}","title":"Metrics"},{"location":"reference/endpoints/smtp/","text":"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 read_timeout 10m write_timeout 1m 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 ocnfiguration. destination example.org { deliver_to &local_mailboxes } default_destination { reject } } Configuration directives Syntax : hostname string Default : global directive value Server name to use in SMTP banner. 220 example.org ESMTP Service Ready Syntax : tls certificate_path key_path { ... } Default : global directive value TLS certificate & key to use. Fine-tuning of other TLS properties is possible by specifing a configuration block and options inside it: tls cert.crt key.key { protocols tls1.2 tls1.3 } See TLS configuration / Server for details. Syntax : io_debug boolean Default : no Write all commands and responses to stderr. Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : insecure_auth boolean Default : no (yes if TLS is disabled) Allow plain-text authentication over unencrypted connections. Not recommended! Syntax : read_timeout duration Default : 10m I/O read timeout. Syntax : write_timeout duration Default : 1m I/O write timeout. Syntax : max_message_size size Default : 32M Limit the size of incoming messages to 'size'. Syntax : max_header_size size Default : 1M Limit the size of incoming message headers to 'size'. Syntax : auth module_reference Default : not specified Use the specified module for authentication. Syntax : 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. Syntax : 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 dictonary attacks (address probing). Syntax : 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\"). Syntax : buffer ram buffer fs [path] buffer auto max_size [path] Default : auto 1M StateDirectory/buffer Temporary storage to use for the body of accepted messages. ram Store the body in RAM. fs Write out the message to the FS and read it back as needed. path can be omitted and defaults to StateDirectory/buffer. auto Store message bodies smaller than max_size entirely in RAM, otherwise write them out to the FS. path can be omitted and defaults to StateDirectory/buffer. Syntax : 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. Syntax : 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 Syntax : limits config block 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: Rate limit Syntax : scope rate burst [period] Restrict the amount of messages processed in period to burst messages. If period is not specified, 1 second is used. Concurrency limit Syntax : scope concurrency max 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 in a scond. \"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, additionaly 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.","title":"SMTP/LMTP/Submission endpoint"},{"location":"reference/endpoints/smtp/#smtplmtpsubmission-endpoint","text":"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 read_timeout 10m write_timeout 1m 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 ocnfiguration. destination example.org { deliver_to &local_mailboxes } default_destination { reject } }","title":"SMTP/LMTP/Submission endpoint"},{"location":"reference/endpoints/smtp/#configuration-directives","text":"Syntax : hostname string Default : global directive value Server name to use in SMTP banner. 220 example.org ESMTP Service Ready Syntax : tls certificate_path key_path { ... } Default : global directive value TLS certificate & key to use. Fine-tuning of other TLS properties is possible by specifing a configuration block and options inside it: tls cert.crt key.key { protocols tls1.2 tls1.3 } See TLS configuration / Server for details. Syntax : io_debug boolean Default : no Write all commands and responses to stderr. Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : insecure_auth boolean Default : no (yes if TLS is disabled) Allow plain-text authentication over unencrypted connections. Not recommended! Syntax : read_timeout duration Default : 10m I/O read timeout. Syntax : write_timeout duration Default : 1m I/O write timeout. Syntax : max_message_size size Default : 32M Limit the size of incoming messages to 'size'. Syntax : max_header_size size Default : 1M Limit the size of incoming message headers to 'size'. Syntax : auth module_reference Default : not specified Use the specified module for authentication. Syntax : 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. Syntax : 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 dictonary attacks (address probing). Syntax : 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\"). Syntax : buffer ram buffer fs [path] buffer auto max_size [path] Default : auto 1M StateDirectory/buffer Temporary storage to use for the body of accepted messages. ram Store the body in RAM. fs Write out the message to the FS and read it back as needed. path can be omitted and defaults to StateDirectory/buffer. auto Store message bodies smaller than max_size entirely in RAM, otherwise write them out to the FS. path can be omitted and defaults to StateDirectory/buffer. Syntax : 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. Syntax : 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.","title":"Configuration directives"},{"location":"reference/endpoints/smtp/#rate-concurrency-limiting","text":"Syntax : limits config block 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: Rate limit Syntax : scope rate burst [period] Restrict the amount of messages processed in period to burst messages. If period is not specified, 1 second is used. Concurrency limit Syntax : scope concurrency max 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 in a scond. \"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.","title":"Rate & concurrency limiting"},{"location":"reference/endpoints/smtp/#submission-module-submission","text":"Module 'submission' implements all functionality of the 'smtp' module and adds certain message preprocessing on top of it, additionaly 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 ... }","title":"Submission module (submission)"},{"location":"reference/endpoints/smtp/#lmtp-module-lmtp","text":"Module 'lmtp' implements all functionality of the 'smtp' module but uses LMTP (RFC 2033) protocol. lmtp unix://lmtp.sock { # ... same as smtp ... }","title":"LMTP module (lmtp)"},{"location":"reference/endpoints/smtp/#limitations-of-lmtp-implementation","text":"Can't be used with TCP. Delivery to 'sql' module storage is always atomic, either all recipients will succeed or none of them will.","title":"Limitations of LMTP implementation"},{"location":"reference/modifiers/dkim/","text":"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 } Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : domains string list Default : not specified REQUIRED. ADministrative Management Domains (ADMDs) taking responsibility for messages. Should be specified either as a directive or as an argument. Syntax : selector string Default : not specified REQUIRED. Identifier of used key within the ADMD. Should be specified either as a directive or as an argument. Syntax : 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. Syntax : 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 Syntax : sign_fields list... Default : see below Header fields that should be signed n+1 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 Syntax : 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. Syntax : 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. Syntax : sig_expiry duration Default : 120h Time for which signature should be considered valid. Mainly used to prevent unauthorized resending of old messages. Syntax : hash hash Default : sha256 Hash algorithm to use when computing body hash. sha256 is the only supported algorithm now. Syntax : newkey_algo rsa4096|rsa2048|ed25519 Default : rsa2048 Algorithm to use when generating a new key. Currently ed25519 is NOT supported by most platforms. Syntax : 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). Syntax : allow_multiple_from boolean Default : no Allow multiple addresses in From header field for purposes of require_sender_match checks. Only first address will be checked, however. Syntax : sign_subdomains boolean Default : no Sign emails from subdomains using a top domain key. Allows only one domain to be specified (can be workarounded using modify.dkim multiple times).","title":"DKIM signing"},{"location":"reference/modifiers/dkim/#dkim-signing","text":"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.","title":"DKIM signing"},{"location":"reference/modifiers/dkim/#arguments","text":"domains and selector can be specified in arguments, so actual modify.dkim use can be shortened to the following: modify { dkim example.org selector }","title":"Arguments"},{"location":"reference/modifiers/dkim/#configuration-directives","text":"modify.dkim { debug no domains example.org example.com selector default key_path dkim-keys/{domain}-{selector}.key oversign_fields ... sign_fields ... header_canon relaxed body_canon relaxed sig_expiry 120h # 5 days hash sha256 newkey_algo rsa2048 } Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : domains string list Default : not specified REQUIRED. ADministrative Management Domains (ADMDs) taking responsibility for messages. Should be specified either as a directive or as an argument. Syntax : selector string Default : not specified REQUIRED. Identifier of used key within the ADMD. Should be specified either as a directive or as an argument. Syntax : 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. Syntax : 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 Syntax : sign_fields list... Default : see below Header fields that should be signed n+1 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 Syntax : 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. Syntax : 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. Syntax : sig_expiry duration Default : 120h Time for which signature should be considered valid. Mainly used to prevent unauthorized resending of old messages. Syntax : hash hash Default : sha256 Hash algorithm to use when computing body hash. sha256 is the only supported algorithm now. Syntax : newkey_algo rsa4096|rsa2048|ed25519 Default : rsa2048 Algorithm to use when generating a new key. Currently ed25519 is NOT supported by most platforms. Syntax : 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). Syntax : allow_multiple_from boolean Default : no Allow multiple addresses in From header field for purposes of require_sender_match checks. Only first address will be checked, however. Syntax : sign_subdomains boolean Default : no Sign emails from subdomains using a top domain key. Allows only one domain to be specified (can be workarounded using modify.dkim multiple times).","title":"Configuration directives"},{"location":"reference/modifiers/envelope/","text":"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 arguments] { [extended table config] } replace_sender
[table arguments] { [extended table config] } Use examples: modify { replace_rcpt file /etc/maddy/aliases replace_rcpt static { entry a@example.org b@example.org entry c@example.org c1@example.org c2@example.org } replace_rcpt regexp \"(.+)@example.net\" \"$1@example.org\" replace_rcpt regexp \"(.+)@example.net\" \"$1@example.org\" \"$1@example.com\" } Possible contents of /etc/maddy/aliases in the example above: # Replace 'cat' with any domain to 'dog'. # E.g. cat@example.net -> dog@example.net cat: dog # Replace cat@example.org with cat@example.com. # Takes priority over the previous line. cat@example.org: cat@example.com # Using aliases in multiple lines cat2: dog cat2: mouse cat2@example.org: cat@example.com cat2@example.org: cat@example.net # Comma-separated aliases in multiple lines cat3: dog , mouse cat3@example.org: cat@example.com , cat@example.net","title":"Envelope sender / recipient rewriting"},{"location":"reference/modifiers/envelope/#envelope-sender-recipient-rewriting","text":"'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 arguments] { [extended table config] } replace_sender
[table arguments] { [extended table config] } Use examples: modify { replace_rcpt file /etc/maddy/aliases replace_rcpt static { entry a@example.org b@example.org entry c@example.org c1@example.org c2@example.org } replace_rcpt regexp \"(.+)@example.net\" \"$1@example.org\" replace_rcpt regexp \"(.+)@example.net\" \"$1@example.org\" \"$1@example.com\" } Possible contents of /etc/maddy/aliases in the example above: # Replace 'cat' with any domain to 'dog'. # E.g. cat@example.net -> dog@example.net cat: dog # Replace cat@example.org with cat@example.com. # Takes priority over the previous line. cat@example.org: cat@example.com # Using aliases in multiple lines cat2: dog cat2: mouse cat2@example.org: cat@example.com cat2@example.org: cat@example.net # Comma-separated aliases in multiple lines cat3: dog , mouse cat3@example.org: cat@example.com , cat@example.net","title":"Envelope sender / recipient rewriting"},{"location":"reference/storage/imap-filters/","text":"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 eariler 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 acccess 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: Junk In this case, message will be placed in the Junk folder. $Label1 In this case, message will be placed in inbox and will have '$Label1' added.","title":"IMAP filters"},{"location":"reference/storage/imap-filters/#imap-filters","text":"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 eariler 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} } }","title":"IMAP filters"},{"location":"reference/storage/imap-filters/#system-command-filter-imapfiltercommand","text":"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 acccess 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: Junk In this case, message will be placed in the Junk folder. $Label1 In this case, message will be placed in inbox and will have '$Label1' added.","title":"System command filter (imap.filter.command)"},{"location":"reference/storage/imapsql/","text":"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 \".*\" \"catchall@example.org\" } deliver_to &local_mailboxes } Arguments Specify the driver and DSN. Configuration directives Syntax : driver string Default : not specified REQUIRED. Use a specified driver to communicate with the database. Supported values: sqlite3, postgres. Should be specified either via an argument or via this directive. Syntax : dsn string Default : not specified REQUIRED. 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. Syntax : msg_store store Default : fs messages/ Module to use for message bodies storage. See \"Blob storage\" section for what you can use here. Syntax : compression off compression algorithm compression algorithm level Default : off Apply compression to message contents. Supported algorithms: lz4, zstd. Syntax : 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. Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : 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. Syntax : 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. Syntax : 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. Syntax : sqlite_busy_timeout integer Default : 5000000 SQLite-specific performance tuning option. Amount of milliseconds to wait before giving up on DB lock. Syntax : 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} } Syntax: delivery_map table Default: identity Use specified table module to map recipient addresses from incoming messages to mailbox names. Normalization algorithm specified in delivery_normalize is appied before delivery_map. Syntax: delivery_normalize name Default: precis_casefold_email Normalization function to apply to email addresses before mapping them to mailboxes. See auth_normalize. Syntax : auth_map table Default : identity Use specified table module to map authentication usernames to mailbox names. Normalization algorithm specified in auth_normalize is applied before auth_map. Syntax : auth_normalize name Default : precis_casefold_email Normalization function to apply to authentication usernames before mapping them to mailboxes. Available options: - precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain - precis_casefold PRECIS UsernameCaseMapped profile for the entire string - precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain - precis PRECIS UsernameCasePreserved profile for the entire string - casefold Convert to lower case - noop Nothing Note: On message delivery, recipient address is unconditionally normalized using precis_casefold_email function.","title":"SQL-indexed storage"},{"location":"reference/storage/imapsql/#sql-indexed-storage","text":"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 \".*\" \"catchall@example.org\" } deliver_to &local_mailboxes }","title":"SQL-indexed storage"},{"location":"reference/storage/imapsql/#arguments","text":"Specify the driver and DSN.","title":"Arguments"},{"location":"reference/storage/imapsql/#configuration-directives","text":"Syntax : driver string Default : not specified REQUIRED. Use a specified driver to communicate with the database. Supported values: sqlite3, postgres. Should be specified either via an argument or via this directive. Syntax : dsn string Default : not specified REQUIRED. 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. Syntax : msg_store store Default : fs messages/ Module to use for message bodies storage. See \"Blob storage\" section for what you can use here. Syntax : compression off compression algorithm compression algorithm level Default : off Apply compression to message contents. Supported algorithms: lz4, zstd. Syntax : 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. Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : 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. Syntax : 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. Syntax : 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. Syntax : sqlite_busy_timeout integer Default : 5000000 SQLite-specific performance tuning option. Amount of milliseconds to wait before giving up on DB lock. Syntax : 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} } Syntax: delivery_map table Default: identity Use specified table module to map recipient addresses from incoming messages to mailbox names. Normalization algorithm specified in delivery_normalize is appied before delivery_map. Syntax: delivery_normalize name Default: precis_casefold_email Normalization function to apply to email addresses before mapping them to mailboxes. See auth_normalize. Syntax : auth_map table Default : identity Use specified table module to map authentication usernames to mailbox names. Normalization algorithm specified in auth_normalize is applied before auth_map. Syntax : auth_normalize name Default : precis_casefold_email Normalization function to apply to authentication usernames before mapping them to mailboxes. Available options: - precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain - precis_casefold PRECIS UsernameCaseMapped profile for the entire string - precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain - precis PRECIS UsernameCasePreserved profile for the entire string - casefold Convert to lower case - noop Nothing Note: On message delivery, recipient address is unconditionally normalized using precis_casefold_email function.","title":"Configuration directives"},{"location":"reference/table/auth/","text":"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.","title":"Authentication providers"},{"location":"reference/table/auth/#authentication-providers","text":"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.","title":"Authentication providers"},{"location":"reference/table/chain/","text":"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 \"(.+)(\\\\+[^+\"@]+)?@example.org\" \"$1@example.org\" step file /etc/maddy/emails } This will strip +prefix from mailbox before looking it up in /etc/maddy/emails list. Configuration directives Syntax : step _table_ Adds a table module to the chain. If input value is not in the table (e.g. file) - return \"not exists\" error. Syntax : 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 regexp \"(.+)@(.+)\" \"$1\" }","title":"Table chaining"},{"location":"reference/table/chain/#table-chaining","text":"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 \"(.+)(\\\\+[^+\"@]+)?@example.org\" \"$1@example.org\" step file /etc/maddy/emails } This will strip +prefix from mailbox before looking it up in /etc/maddy/emails list.","title":"Table chaining"},{"location":"reference/table/chain/#configuration-directives","text":"Syntax : step _table_ Adds a table module to the chain. If input value is not in the table (e.g. file) - return \"not exists\" error. Syntax : 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 regexp \"(.+)@(.+)\" \"$1\" }","title":"Configuration directives"},{"location":"reference/table/email_localpart/","text":"Email local part The module 'table.email_localpart' extracts and unescaped local (\"username\") part of the email address. E.g. test@example.org => test \"test @ a\"@example.org => test @ a table.email_localpart { }","title":"Email local part"},{"location":"reference/table/email_localpart/#email-local-part","text":"The module 'table.email_localpart' extracts and unescaped local (\"username\") part of the email address. E.g. test@example.org => test \"test @ a\"@example.org => test @ a table.email_localpart { }","title":"Email local part"},{"location":"reference/table/file/","text":"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 or file { file } 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, secondvalue","title":"File"},{"location":"reference/table/file/#file","text":"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 or file { file } Usage example: # Resolve SMTP address aliases using text file mapping. modify { replace_rcpt file /etc/maddy/aliases }","title":"File"},{"location":"reference/table/file/#syntax","text":"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, secondvalue","title":"Syntax"},{"location":"reference/table/regexp/","text":"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 [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 Syntax : 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. Syntax : case_insensitive boolean Default : yes Whether to make matching case-insensitive. Syntax : 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 { }","title":"Regexp rewrite table"},{"location":"reference/table/regexp/#regexp-rewrite-table","text":"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 [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\" { ... }","title":"Regexp rewrite table"},{"location":"reference/table/regexp/#configuration-directives","text":"Syntax : 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. Syntax : case_insensitive boolean Default : yes Whether to make matching case-insensitive. Syntax : 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.","title":"Configuration directives"},{"location":"reference/table/regexp/#identity-table-tableidentity","text":"The module 'identity' is a table module that just returns the key looked up. table.identity { }","title":"Identity table (table.identity)"},{"location":"reference/table/sql_query/","text":"SQL query mapping The table.sql_query module implements table interface using SQL queries. Definition: table.sql_query { driver dsn lookup # Optional: init list add del set } 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 Syntax : driver driver name REQUIRED Driver to use to access the database. Supported drivers: postgres, sqlite3 (if compiled with C support) Syntax : 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 Syntax : 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. Syntax : 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\" } Syntax: 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 Syntax: add query Syntax: list query Syntax: set query Syntax: 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).","title":"SQL query mapping"},{"location":"reference/table/sql_query/#sql-query-mapping","text":"The table.sql_query module implements table interface using SQL queries. Definition: table.sql_query { driver dsn lookup # Optional: init list add del set } 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\" } }","title":"SQL query mapping"},{"location":"reference/table/sql_query/#configuration-directives","text":"Syntax : driver driver name REQUIRED Driver to use to access the database. Supported drivers: postgres, sqlite3 (if compiled with C support) Syntax : 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 Syntax : 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. Syntax : 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\" } Syntax: 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 Syntax: add query Syntax: list query Syntax: set query Syntax: 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).","title":"Configuration directives"},{"location":"reference/table/static/","text":"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 Syntax : entry key _value_ Add an entry to the table. If the same key is used multiple times, the last one takes effect.","title":"Static table"},{"location":"reference/table/static/#static-table","text":"The 'static' module implements table lookups using key-value pairs in its configuration. table.static { entry KEY1 VALUE1 entry KEY2 VALUE2 ... }","title":"Static table"},{"location":"reference/table/static/#configuration-directives","text":"Syntax : entry key _value_ Add an entry to the table. If the same key is used multiple times, the last one takes effect.","title":"Configuration directives"},{"location":"reference/targets/queue/","text":"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 } Syntax : target block_name Default : not specified REQUIRED. Delivery target to use for final delivery. Syntax : location directory Default : StateDirectory/configuration_block_name File system directory to use to store queued messages. Relative paths are relative to the StateDirectory. Syntax : 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. Syntax : max_tries integer Default : 20 Attempt delivery up to integer times. Note that no more attempts will be done is permanent error occured during previous attempt. Delay before the next attempt will be increased exponentally 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, ... Syntax : bounce { ... } Default : not specified This configuration contains pipeline configuration to be used for generated DSN (Delivery Status Notifiaction) 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. Syntax : 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. Syntax : debug boolean Default : no Enable verbose logging.","title":"Local queue"},{"location":"reference/targets/queue/#local-queue","text":"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.","title":"Local queue"},{"location":"reference/targets/queue/#arguments","text":"First argument specifies directory to use for storage. Relative paths are relative to the StateDirectory.","title":"Arguments"},{"location":"reference/targets/queue/#configuration-directives","text":"target.queue { target remote location ... max_parallelism 16 max_tries 4 bounce { destination example.org { deliver_to &local_mailboxes } default_destination { reject } } autogenerated_msg_domain example.org debug no } Syntax : target block_name Default : not specified REQUIRED. Delivery target to use for final delivery. Syntax : location directory Default : StateDirectory/configuration_block_name File system directory to use to store queued messages. Relative paths are relative to the StateDirectory. Syntax : 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. Syntax : max_tries integer Default : 20 Attempt delivery up to integer times. Note that no more attempts will be done is permanent error occured during previous attempt. Delay before the next attempt will be increased exponentally 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, ... Syntax : bounce { ... } Default : not specified This configuration contains pipeline configuration to be used for generated DSN (Delivery Status Notifiaction) 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. Syntax : 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. Syntax : debug boolean Default : no Enable verbose logging.","title":"Configuration directives"},{"location":"reference/targets/remote/","text":"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 } Syntax : 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. Syntax : limits config block 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. Syntax : local_ip IP address Default : empty Choose the local IP to bind for outbound SMTP connections. Syntax : 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. Syntax : 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. Syntax : 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. Syntax : submission_timeout duration Default : 12m Time to wait after the entire message is sent (after \"final dot\"). RFC 5321 recommends 10 minutes. Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : 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). Syntax : 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. Syntax : 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. Syntax : conn_max_idle_count integer Default : 10 Max. amount of idle connections per recipient domains to keep in cache. Syntax : conn_max_idle_time integer Default : 150 (2.5 min) Amount of time the idle connection is still considered potentially usable. Security policies Syntax : mx_auth config block 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 numberous 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 { ... } } 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 } Syntax : 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. Syntax : 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 reslies on the upstream resolver to do so by causing lookup to fail when verification fails and setting the AD flag for signed and verfified 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'. Syntax : min_tls_level none|encrypted|authenticated Default : none Set the minimal TLS security level required for all outbound messages. See Security levels page for details. Syntax : 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.","title":"Remote MX delivery"},{"location":"reference/targets/remote/#remote-mx-delivery","text":"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.","title":"Remote MX delivery"},{"location":"reference/targets/remote/#configuration-directives","text":"target.remote { hostname mx.example.org debug no } Syntax : 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. Syntax : limits config block 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. Syntax : local_ip IP address Default : empty Choose the local IP to bind for outbound SMTP connections. Syntax : 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. Syntax : 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. Syntax : 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. Syntax : submission_timeout duration Default : 12m Time to wait after the entire message is sent (after \"final dot\"). RFC 5321 recommends 10 minutes. Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : 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). Syntax : 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. Syntax : 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. Syntax : conn_max_idle_count integer Default : 10 Max. amount of idle connections per recipient domains to keep in cache. Syntax : conn_max_idle_time integer Default : 150 (2.5 min) Amount of time the idle connection is still considered potentially usable.","title":"Configuration directives"},{"location":"reference/targets/remote/#security-policies","text":"Syntax : mx_auth config block 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 numberous 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 { ... } }","title":"Security policies"},{"location":"reference/targets/remote/#mta-sts","text":"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 } Syntax : 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. Syntax : fs_dir directory Default : StateDirectory/mtasts_cache Filesystem directory to use for policies caching if 'cache' is set to 'fs'.","title":"MTA-STS"},{"location":"reference/targets/remote/#dnssec","text":"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 reslies on the upstream resolver to do so by causing lookup to fail when verification fails and setting the AD flag for signed and verfified 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 { }","title":"DNSSEC"},{"location":"reference/targets/remote/#dane","text":"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 { }","title":"DANE"},{"location":"reference/targets/remote/#local-policy","text":"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'. Syntax : min_tls_level none|encrypted|authenticated Default : none Set the minimal TLS security level required for all outbound messages. See Security levels page for details. Syntax : 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.","title":"Local policy"},{"location":"reference/targets/smtp/","text":"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 } Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : tls_client { ... } Default : not specified Advanced TLS client configuration options. See TLS configuration / Client for details. Syntax : attempt_starttls boolean Default : yes (no for target.lmtp) Attempt to use STARTTLS if it is supported by the remote server. If TLS handshake fails, connection will be retried without STARTTLS unless 'require_tls' is also specified. Syntax : require_tls boolean Default : no Refuse to pass messages over plain-text connections. Syntax : 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. Syntax : targets endpoints... Default: not specified REQUIRED. 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). Syntax : connect_timeout duration Default : 5m Same as for target.remote. Syntax : command_timeout duration Default : 5m Same as for target.remote. Syntax : submission_timeout duration Default : 12m Same as for target.remote.","title":"SMTP & LMTP transparent forwarding"},{"location":"reference/targets/smtp/#smtp-lmtp-transparent-forwarding","text":"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 .","title":"SMTP & LMTP transparent forwarding"},{"location":"reference/targets/smtp/#configuration-directives","text":"target.smtp { debug no tls_client { ... } attempt_starttls yes require_tls no auth off targets tcp://127.0.0.1:2525 connect_timeout 5m command_timeout 5m submission_timeout 12m } Syntax : debug boolean Default : global directive value Enable verbose logging. Syntax : tls_client { ... } Default : not specified Advanced TLS client configuration options. See TLS configuration / Client for details. Syntax : attempt_starttls boolean Default : yes (no for target.lmtp) Attempt to use STARTTLS if it is supported by the remote server. If TLS handshake fails, connection will be retried without STARTTLS unless 'require_tls' is also specified. Syntax : require_tls boolean Default : no Refuse to pass messages over plain-text connections. Syntax : 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. Syntax : targets endpoints... Default: not specified REQUIRED. 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). Syntax : connect_timeout duration Default : 5m Same as for target.remote. Syntax : command_timeout duration Default : 5m Same as for target.remote. Syntax : submission_timeout duration Default : 12m Same as for target.remote.","title":"Configuration directives"},{"location":"third-party/dovecot/","text":"Dovecot Builtin maddy IMAP server may not match your requirements in terms of performance, reliabilty 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 imap endpoint and existing local_authdb and local_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 servers'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 } }","title":"Dovecot"},{"location":"third-party/dovecot/#dovecot","text":"Builtin maddy IMAP server may not match your requirements in terms of performance, reliabilty 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 imap endpoint and existing local_authdb and local_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 }","title":"Dovecot"},{"location":"third-party/dovecot/#authentication","text":"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 ... }","title":"Authentication"},{"location":"third-party/dovecot/#other-imap-servers","text":"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 servers'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 } }","title":"Other IMAP servers"},{"location":"third-party/mailman3/","text":"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: 8024 After that, you will need to configure maddy to send messages to Mailman. The preferrable 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))?@lists.example.org\" { deliver_to lmtp tcp://127.0.0.1:8024 } destination_in regexp \"second-mailinglist(-(bounces\\+.*|confirm\\+.*|join|leave|owner|request|subscribe|unsubscribe))?@lists.example.org\" { 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: mailman@example.org smtp_pass: something-very-secret smtp_secure_mode: smtps If you do not want to use TLS and/or authentication you can create a separate endpoint and just point Mailman to it. E.g. 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.","title":"Mailman 3"},{"location":"third-party/mailman3/#mailman-3","text":"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.","title":"Mailman 3"},{"location":"third-party/mailman3/#accepting-messages","text":"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: 8024 After that, you will need to configure maddy to send messages to Mailman. The preferrable 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))?@lists.example.org\" { deliver_to lmtp tcp://127.0.0.1:8024 } destination_in regexp \"second-mailinglist(-(bounces\\+.*|confirm\\+.*|join|leave|owner|request|subscribe|unsubscribe))?@lists.example.org\" { 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...)","title":"Accepting messages"},{"location":"third-party/mailman3/#sending-messages","text":"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: mailman@example.org smtp_pass: something-very-secret smtp_secure_mode: smtps If you do not want to use TLS and/or authentication you can create a separate endpoint and just point Mailman to it. E.g. 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.","title":"Sending messages"},{"location":"third-party/rspamd/","text":"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): checks { ... 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","title":"rspamd"},{"location":"third-party/rspamd/#rspamd","text":"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): checks { ... 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","title":"rspamd"},{"location":"third-party/smtp-servers/","text":"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 authentiation 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 }","title":"External SMTP server"},{"location":"third-party/smtp-servers/#external-smtp-server","text":"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 authentiation 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 }","title":"External SMTP server"},{"location":"tutorials/alias-to-remote/","text":"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): foxcpp@example.org: foxcpp@example.com You will get \"User does not exist\" error when attempting to send a message to foxcpp@example.org because foxcpp@example.com 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 foxcpp@example.com 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 queueing 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_queue with deliver_to &outbound_delivery (assuming outbound_delivery refers to target.remote block)","title":"Forward messages to a remote MX"},{"location":"tutorials/alias-to-remote/#forward-messages-to-a-remote-mx","text":"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): foxcpp@example.org: foxcpp@example.com You will get \"User does not exist\" error when attempting to send a message to foxcpp@example.org because foxcpp@example.com 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 foxcpp@example.com 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\" } }","title":"Forward messages to a remote MX"},{"location":"tutorials/alias-to-remote/#bounce-handling","text":"Once the message is delivered to remote_queue , it will follow the usual path for outbound delivery, including queueing 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.","title":"Bounce handling"},{"location":"tutorials/alias-to-remote/#transparent-forwarding","text":"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_queue with deliver_to &outbound_delivery (assuming outbound_delivery refers to target.remote block)","title":"Transparent forwarding"},{"location":"tutorials/building-from-source/","text":"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.17 gcc libc6-dev make Additionally, if you want manual pages, you should also have scdoc installed. Figuring out the appropriate way to get scdoc is left as an exercise for reader (for Ubuntu 19.10 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 ). It should not be hard to grab a recent built toolchain from golang.org: wget \"https://dl.google.com/go/go1.17.11.linux-amd64.tar.gz\" tar xf \"go1.17.11.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.6.0 # a specific release $ git checkout master # next bugfix release $ git checkout dev # next feature release Build & install it $ ./build.sh # ./build.sh install Have fun!","title":"Building from source"},{"location":"tutorials/building-from-source/#building-from-source","text":"","title":"Building from source"},{"location":"tutorials/building-from-source/#system-dependencies","text":"You need C toolchain, Go toolchain and Make: On Debian-based system this should work: apt-get install golang-1.17 gcc libc6-dev make Additionally, if you want manual pages, you should also have scdoc installed. Figuring out the appropriate way to get scdoc is left as an exercise for reader (for Ubuntu 19.10 it is in repositories).","title":"System dependencies"},{"location":"tutorials/building-from-source/#recent-go-toolchain","text":"maddy depends on a rather recent Go toolchain version that may not be available in some distributions ( cough Debian cough ). It should not be hard to grab a recent built toolchain from golang.org: wget \"https://dl.google.com/go/go1.17.11.linux-amd64.tar.gz\" tar xf \"go1.17.11.linux-amd64.tar.gz\" export GOROOT=\"$PWD/go\" export PATH=\"$PWD/go/bin:$PATH\"","title":"Recent Go toolchain"},{"location":"tutorials/building-from-source/#step-by-step","text":"Clone repository $ git clone https://github.com/foxcpp/maddy.git $ cd maddy Select the appropriate version to build: $ git checkout v0.6.0 # a specific release $ git checkout master # next bugfix release $ git checkout dev # next feature release Build & install it $ ./build.sh # ./build.sh install Have fun!","title":"Step-by-step"},{"location":"tutorials/pam/","text":"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-helper Copy 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-helper Then 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 also configure storage backend to use username only as an account identifier, not full email addresses: storage.imapsql local_mailboxes { ... delivery_map email_localpart auth_normalize precis_casefold } This way, when authenticating as foxcpp , it will be mapped to foxcpp storage account. E.g. you will need to run maddy imap-accts create foxcpp , without the domain part. If you have existing accounts, you will need to rename them. Change to auth_normalize is necessary so that normalization function will not attempt to parse authentication identity as a email. When a email is received, delivery_map email_localpart will strip the domain part before looking up the account. That is, foxcpp@example.org will be become just foxcpp . You also need to make authorize_sender check (used in submission endpoint) accept non-email usernames: authorize_sender { ... auth_normalize precis_casefold user_to_email regexp \"(.*)\" \"$1@$(primary_domain)\" } Note that is would work only if clients use only one domain as sender ( $(primary_domain) ). If you want to allow sending from all domains, you need to remove authorize_sender check altogether since it is not currently supported. 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.so Here 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-session","title":"Using PAM authentication"},{"location":"tutorials/pam/#using-pam-authentication","text":"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.","title":"Using PAM authentication"},{"location":"tutorials/pam/#built-in-pam-support","text":"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 }","title":"Built-in PAM support"},{"location":"tutorials/pam/#helper-executable","text":"TL;DR git clone https://github.com/foxcpp/maddy cd maddy/cmd/maddy-pam-helper gcc pam.c main.c -lpam -o maddy-pam-helper Copy the resulting executable into /usr/lib/maddy/ and make it setuid-root so it can read /etc/shadow (if that's necessary): chown root:maddy /usr/lib/maddy/maddy-pam-helper chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helper Then you should be able to replace local_authdb implementation in default configuration with auth.pam : auth.pam local_authdb { use_helper yes }","title":"Helper executable"},{"location":"tutorials/pam/#account-names","text":"Since PAM does not use emails for authentication you should also configure storage backend to use username only as an account identifier, not full email addresses: storage.imapsql local_mailboxes { ... delivery_map email_localpart auth_normalize precis_casefold } This way, when authenticating as foxcpp , it will be mapped to foxcpp storage account. E.g. you will need to run maddy imap-accts create foxcpp , without the domain part. If you have existing accounts, you will need to rename them. Change to auth_normalize is necessary so that normalization function will not attempt to parse authentication identity as a email. When a email is received, delivery_map email_localpart will strip the domain part before looking up the account. That is, foxcpp@example.org will be become just foxcpp . You also need to make authorize_sender check (used in submission endpoint) accept non-email usernames: authorize_sender { ... auth_normalize precis_casefold user_to_email regexp \"(.*)\" \"$1@$(primary_domain)\" } Note that is would work only if clients use only one domain as sender ( $(primary_domain) ). If you want to allow sending from all domains, you need to remove authorize_sender check altogether since it is not currently supported.","title":"Account names"},{"location":"tutorials/pam/#pam-service","text":"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.so Here 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-session","title":"PAM service"},{"location":"tutorials/setting-up/","text":"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) docker pull foxcpp/maddy:0.6 See here for Docker-specific instructions. Building from source See here for instructions. Arch Linux packages For Arch Linux users, maddy and maddy-git PKGBUILDs are available in AUR. Additionally, binary packages are available in 3rd-party repository at https://maddy.email/archlinux/ System configuration (systemd-based distribution) If you built maddy from source and used ./build.sh install then systemd unit files should be already installed. If you used a pre-built tarball - copy systemd/*.service to /etc/systemd/system manually. You need to reload service manager configuration to make service available: systemctl daemon-reload Additionally, 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\" maddy Host name + domain Open /etc/maddy/maddy.conf with vim^W your favorite editor and change the following lines to match your server name and domain you want to handle mail for. If you setup a very small mail server you can use example.org in both fields. However, to easier a future migration of service, it's recommended to use a separate DNS entry for that purpose. It's usually mx1.example.org, mx2, etc. You can of course use another subdomain, for instance: smtp1.example.org. An email failover server will become possible if you forward mx2.example.org to another server (as long as you configure it to handle your domain). $(hostname) = mx1.example.org $(primary_domain) = example.org If you want to handle multiple domains, you still need to designate one as \"primary\". Add all other domains to the local_domains line: $(local_domains) = $(primary_domain) example.com other.example.com TLS certificates One thing that can't be automagically 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.key maddy reloads TLS certificates from disk once in a minute so it will notice renewal. It is possible to force reload via systemctl reload maddy (or just killall -USR2 maddy ). Let's Encrypt and certbot If you use certbot to manage your certificates, you can simply symlink /etc/maddy/certs into /etc/letsencrypt/live. maddy will pick the right certificate depending on the domain you specified during installation. You still need to make keys readable for maddy, though: $ 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.pem First run systemctl start maddy The daemon should be running now, except that it is useless because we haven't configured DNS records. DNS records How it is configured depends on your DNS provider (or server, if you run your own). Here is how your DNS zone should look like: ; 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 mx ~all\" ; Opt-in into DMARC with permissive policy and request reports about broken ; messages. _dmarc.example.org. TXT \"v=DMARC1; p=quarantine; ruf=mailto:postmaster@example.org\" ; Mark domain as MTA-STS compatible (see the next section) ; and request reports about failures to be sent to postmaster@example.org _mta-sts.example.org. TXT \"v=STSv1; id=1\" _smtp._tls.example.org. TXT \"v=TLSRPTv1;rua=mailto:postmaster@example.org\" 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.org Note : mx1.example.org in the file is your MX hostname, In a simple configuration, it will be the same as your hostname example.org. In a more complex setups, you would have multiple MX servers - add them all once per line, like that: mx: mx1.example.org mx: mx2.example.org It is also recommended to set a TLSA (DANE) record. Use https://www.huque.com/bin/gen_tlsa to generate one. Set port to 25, Transport Protocol to \"tcp\" and Domain Name to the MX hostname . Example of a valid record: _25._tcp.mx1.example.org. TLSA 3 1 1 7f59d873a70e224b184c95a4eb54caa9621e47d48b4a25d312d83d96e3498238 User accounts and maddy command A mail server is useless without mailboxes, right? Unlike software like postfix and dovecot, maddy uses \"virtual users\" by default, meaning it does not care or know about system users. IMAP mailboxes (\"accounts\") and authentication credentials are kept separate. To register user credentials, use maddy creds create command. Like that: $ maddy creds create postmaster@example.org 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 postmaster@example.org That is it. Now you have your first e-mail address. when authenticating using your e-mail client, do not forget the username is \"postmaster@example.org\", 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.","title":"Installation & initial configuration"},{"location":"tutorials/setting-up/#installation-initial-configuration","text":"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","title":"Installation & initial configuration"},{"location":"tutorials/setting-up/#getting-a-server","text":"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.","title":"Getting a server"},{"location":"tutorials/setting-up/#installing-maddy","text":"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) docker pull foxcpp/maddy:0.6 See here for Docker-specific instructions. Building from source See here for instructions. Arch Linux packages For Arch Linux users, maddy and maddy-git PKGBUILDs are available in AUR. Additionally, binary packages are available in 3rd-party repository at https://maddy.email/archlinux/","title":"Installing maddy"},{"location":"tutorials/setting-up/#system-configuration-systemd-based-distribution","text":"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-reload Additionally, 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\" maddy","title":"System configuration (systemd-based distribution)"},{"location":"tutorials/setting-up/#host-name-domain","text":"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.org If you want to handle multiple domains, you still need to designate one as \"primary\". Add all other domains to the local_domains line: $(local_domains) = $(primary_domain) example.com other.example.com","title":"Host name + domain"},{"location":"tutorials/setting-up/#tls-certificates","text":"One thing that can't be automagically 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.key maddy reloads TLS certificates from disk once in a minute so it will notice renewal. It is possible to force reload via systemctl reload maddy (or just killall -USR2 maddy ).","title":"TLS certificates"},{"location":"tutorials/setting-up/#lets-encrypt-and-certbot","text":"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}","title":"Let's Encrypt and certbot"},{"location":"tutorials/setting-up/#acmesh","text":"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.pem","title":"ACME.sh"},{"location":"tutorials/setting-up/#first-run","text":"systemctl start maddy The daemon should be running now, except that it is useless because we haven't configured DNS records.","title":"First run"},{"location":"tutorials/setting-up/#dns-records","text":"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 mx ~all\" ; Opt-in into DMARC with permissive policy and request reports about broken ; messages. _dmarc.example.org. TXT \"v=DMARC1; p=quarantine; ruf=mailto:postmaster@example.org\" ; Mark domain as MTA-STS compatible (see the next section) ; and request reports about failures to be sent to postmaster@example.org _mta-sts.example.org. TXT \"v=STSv1; id=1\" _smtp._tls.example.org. TXT \"v=TLSRPTv1;rua=mailto:postmaster@example.org\" 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=\"","title":"DNS records"},{"location":"tutorials/setting-up/#mta-sts-and-dane","text":"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.org Note : mx1.example.org in the file is your MX hostname, In a simple configuration, it will be the same as your hostname example.org. In a more complex setups, you would have multiple MX servers - add them all once per line, like that: mx: mx1.example.org mx: mx2.example.org It is also recommended to set a TLSA (DANE) record. Use https://www.huque.com/bin/gen_tlsa to generate one. Set port to 25, Transport Protocol to \"tcp\" and Domain Name to the MX hostname . Example of a valid record: _25._tcp.mx1.example.org. TLSA 3 1 1 7f59d873a70e224b184c95a4eb54caa9621e47d48b4a25d312d83d96e3498238","title":"MTA-STS and DANE"},{"location":"tutorials/setting-up/#user-accounts-and-maddy-command","text":"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 postmaster@example.org 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 postmaster@example.org That is it. Now you have your first e-mail address. when authenticating using your e-mail client, do not forget the username is \"postmaster@example.org\", 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.","title":"User accounts and maddy command"}]} \ No newline at end of file diff --git a/search/worker.js b/search/worker.js new file mode 100644 index 00000000..8628dbce --- /dev/null +++ b/search/worker.js @@ -0,0 +1,133 @@ +var base_path = 'function' === typeof importScripts ? '.' : '/search/'; +var allowSearch = false; +var index; +var documents = {}; +var lang = ['en']; +var data; + +function getScript(script, callback) { + console.log('Loading script: ' + script); + $.getScript(base_path + script).done(function () { + callback(); + }).fail(function (jqxhr, settings, exception) { + console.log('Error: ' + exception); + }); +} + +function getScriptsInOrder(scripts, callback) { + if (scripts.length === 0) { + callback(); + return; + } + getScript(scripts[0], function() { + getScriptsInOrder(scripts.slice(1), callback); + }); +} + +function loadScripts(urls, callback) { + if( 'function' === typeof importScripts ) { + importScripts.apply(null, urls); + callback(); + } else { + getScriptsInOrder(urls, callback); + } +} + +function onJSONLoaded () { + data = JSON.parse(this.responseText); + var scriptsToLoad = ['lunr.js']; + if (data.config && data.config.lang && data.config.lang.length) { + lang = data.config.lang; + } + if (lang.length > 1 || lang[0] !== "en") { + scriptsToLoad.push('lunr.stemmer.support.js'); + if (lang.length > 1) { + scriptsToLoad.push('lunr.multi.js'); + } + if (lang.includes("ja") || lang.includes("jp")) { + scriptsToLoad.push('tinyseg.js'); + } + for (var i=0; i < lang.length; i++) { + if (lang[i] != 'en') { + scriptsToLoad.push(['lunr', lang[i], 'js'].join('.')); + } + } + } + loadScripts(scriptsToLoad, onScriptsLoaded); +} + +function onScriptsLoaded () { + console.log('All search scripts loaded, building Lunr index...'); + if (data.config && data.config.separator && data.config.separator.length) { + lunr.tokenizer.separator = new RegExp(data.config.separator); + } + + if (data.index) { + index = lunr.Index.load(data.index); + data.docs.forEach(function (doc) { + documents[doc.location] = doc; + }); + console.log('Lunr pre-built index loaded, search ready'); + } else { + index = lunr(function () { + if (lang.length === 1 && lang[0] !== "en" && lunr[lang[0]]) { + this.use(lunr[lang[0]]); + } else if (lang.length > 1) { + this.use(lunr.multiLanguage.apply(null, lang)); // spread operator not supported in all browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator#Browser_compatibility + } + this.field('title'); + this.field('text'); + this.ref('location'); + + for (var i=0; i < data.docs.length; i++) { + var doc = data.docs[i]; + this.add(doc); + documents[doc.location] = doc; + } + }); + console.log('Lunr index built, search ready'); + } + allowSearch = true; + postMessage({config: data.config}); + postMessage({allowSearch: allowSearch}); +} + +function init () { + var oReq = new XMLHttpRequest(); + oReq.addEventListener("load", onJSONLoaded); + var index_path = base_path + '/search_index.json'; + if( 'function' === typeof importScripts ){ + index_path = 'search_index.json'; + } + oReq.open("GET", index_path); + oReq.send(); +} + +function search (query) { + if (!allowSearch) { + console.error('Assets for search still loading'); + return; + } + + var resultDocuments = []; + var results = index.search(query); + for (var i=0; i < results.length; i++){ + var result = results[i]; + doc = documents[result.ref]; + doc.summary = doc.text.substring(0, 200); + resultDocuments.push(doc); + } + return resultDocuments; +} + +if( 'function' === typeof importScripts ) { + onmessage = function (e) { + if (e.data.init) { + init(); + } else if (e.data.query) { + postMessage({ results: search(e.data.query) }); + } else { + console.error("Worker - Unrecognized message: " + e); + } + }; +} diff --git a/seclevels/index.html b/seclevels/index.html new file mode 100644 index 00000000..b9ce38c0 --- /dev/null +++ b/seclevels/index.html @@ -0,0 +1,772 @@ + + + + + + + + +Outbound delivery security - maddy + + + +
+ +
+ +
+ +
+ +

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 +estabilished 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 levelNoneEncryptedAuthenticated
None-PP
MTA-STS-PPA (see note 1)
DNSSEC-PPA
+

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

+ +

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.

+ + +
+ + + + + + + +
+ +
+
+ + + +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..6b7d685b --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,308 @@ + + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + + None + 2022-12-05 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..a2d77cc107d6ae142bc0809f4eaaa4bfc8d7139f GIT binary patch literal 231 zcmb2|=HQsc(3i~gzc{lbH8-(9uOc^x;q6&pz9s_!)(3l+E8M;8BrI&Y^i9mJOUwc* z9GG;CLce_bB;H(JvH9hfuS@59-Om-?Es zk)6cpX+mL@_vf}qv^565^_^XMBsQMw)as8;NxZH{`hDIv6)xkSmMeF4M@;-2Q!k&} z-xQVa?{Yframrv?;ud-B>}@NbhNgKJzd!3N9(lKB$_KI}+kZ1T1_l6yHfcEk literal 0 HcmV?d00001 diff --git a/third-party/dovecot/index.html b/third-party/dovecot/index.html new file mode 100644 index 00000000..f6894767 --- /dev/null +++ b/third-party/dovecot/index.html @@ -0,0 +1,748 @@ + + + + + + + + +Dovecot - maddy + + + +
+ +
+ +
+ +
+ +

Dovecot

+

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

+
    +
  1. +

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

    +
  2. +
  3. +

    Setup Dovecot to provide LMTP endpoint

    +
  4. +
+

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 servers'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
+    }
+}
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/third-party/mailman3/index.html b/third-party/mailman3/index.html new file mode 100644 index 00000000..2bc51227 --- /dev/null +++ b/third-party/mailman3/index.html @@ -0,0 +1,742 @@ + + + + + + + + +Mailman 3 - maddy + + + +
+ +
+ +
+ +
+ +

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: 8024
+
+ +

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

+

The preferrable 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))?@lists.example.org" {
+        deliver_to lmtp tcp://127.0.0.1:8024
+    }
+    destination_in regexp "second-mailinglist(-(bounces\+.*|confirm\+.*|join|leave|owner|request|subscribe|unsubscribe))?@lists.example.org" {
+        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: mailman@example.org
+smtp_pass: something-very-secret
+smtp_secure_mode: smtps
+
+ +

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

+
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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/third-party/rspamd/index.html b/third-party/rspamd/index.html new file mode 100644 index 00000000..17faaf44 --- /dev/null +++ b/third-party/rspamd/index.html @@ -0,0 +1,690 @@ + + + + + + + + +rspamd - maddy + + + +
+ +
+ +
+ +
+ +

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):

+
checks {
+    ...
+    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
  • +
+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/third-party/smtp-servers/index.html b/third-party/smtp-servers/index.html new file mode 100644 index 00000000..b42609dc --- /dev/null +++ b/third-party/smtp-servers/index.html @@ -0,0 +1,708 @@ + + + + + + + + +External SMTP server - maddy + + + +
+ +
+ +
+ +
+ +

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 authentiation 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
+}
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/tutorials/alias-to-remote/index.html b/tutorials/alias-to-remote/index.html new file mode 100644 index 00000000..6af84b25 --- /dev/null +++ b/tutorials/alias-to-remote/index.html @@ -0,0 +1,771 @@ + + + + + + + + +Forward messages to a remote MX - maddy + + + +
+ +
+ +
+ +
+ +

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):

+
foxcpp@example.org: foxcpp@example.com
+
+ +

You will get "User does not exist" error when attempting to send a message to +foxcpp@example.org because foxcpp@example.com 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 foxcpp@example.com 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 queueing 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_queue
+
+ +

with

+
deliver_to &outbound_delivery
+
+ +

(assuming outbound_delivery refers to target.remote block)

+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/tutorials/building-from-source/index.html b/tutorials/building-from-source/index.html new file mode 100644 index 00000000..3f39de8e --- /dev/null +++ b/tutorials/building-from-source/index.html @@ -0,0 +1,719 @@ + + + + + + + + +Building from source - maddy + + + +
+ +
+ +
+ +
+ +

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.17 gcc libc6-dev make
+
+ +

Additionally, if you want manual pages, you should also have scdoc installed. +Figuring out the appropriate way to get scdoc is left as an exercise for +reader (for Ubuntu 19.10 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).

+

It should not be hard to grab a recent built toolchain from golang.org:

+
wget "https://dl.google.com/go/go1.17.11.linux-amd64.tar.gz"
+tar xf "go1.17.11.linux-amd64.tar.gz"
+export GOROOT="$PWD/go"
+export PATH="$PWD/go/bin:$PATH"
+
+ +

Step-by-step

+
    +
  1. Clone repository
  2. +
+
$ git clone https://github.com/foxcpp/maddy.git
+$ cd maddy
+
+ +
    +
  1. Select the appropriate version to build:
  2. +
+
$ git checkout v0.6.0      # a specific release
+$ git checkout master      # next bugfix release
+$ git checkout dev         # next feature release
+
+ +
    +
  1. Build & install it
  2. +
+
$ ./build.sh
+# ./build.sh install
+
+ +
    +
  1. Have fun!
  2. +
+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/tutorials/pam/index.html b/tutorials/pam/index.html new file mode 100644 index 00000000..95872682 --- /dev/null +++ b/tutorials/pam/index.html @@ -0,0 +1,776 @@ + + + + + + + + +Using PAM authentication - maddy + + + +
+ +
+ +
+ +
+ +

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-helper
+
+ +

Copy 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-helper
+
+ +

Then 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 also +configure storage backend to use username only as an account identifier, +not full email addresses:

+
storage.imapsql local_mailboxes {
+    ...
+    delivery_map email_localpart
+    auth_normalize precis_casefold
+}
+
+ +

This way, when authenticating as foxcpp, it will be mapped to +foxcpp storage account. E.g. you will need to run +maddy imap-accts create foxcpp, without the domain part.

+

If you have existing accounts, you will need to rename them.

+

Change to auth_normalize is necessary so that normalization function +will not attempt to parse authentication identity as a email.

+

When a email is received, delivery_map email_localpart will strip +the domain part before looking up the account. That is, +foxcpp@example.org will be become just foxcpp.

+

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

+
authorize_sender {
+  ...
+  auth_normalize precis_casefold
+  user_to_email regexp "(.*)" "$1@$(primary_domain)"
+}
+
+ +

Note that is would work only if clients use only one domain as sender ($(primary_domain)). +If you want to allow sending from all domains, you need to remove authorize_sender check +altogether since it is not currently supported.

+

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.so
+
+ +

Here 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-session
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/tutorials/setting-up/index.html b/tutorials/setting-up/index.html new file mode 100644 index 00000000..39053166 --- /dev/null +++ b/tutorials/setting-up/index.html @@ -0,0 +1,910 @@ + + + + + + + + +Installation & initial configuration - maddy + + + +
+ +
+ +
+ +
+ +

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)

    +

    docker pull foxcpp/maddy:0.6

    +

    See here for Docker-specific instructions.

    +
  • +
  • +

    Building from source

    +

    See here for instructions.

    +
  • +
  • +

    Arch Linux packages

    +

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

    +
  • +
+

System configuration (systemd-based distribution)

+

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

+

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

+
systemctl daemon-reload
+
+ +

Additionally, 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" maddy
+
+ +

Host name + domain

+

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

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

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

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

TLS certificates

+

One thing that can't be automagically 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.key
+
+ +

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

+

Let's Encrypt and certbot

+

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

+

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

+
$ 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.pem
+
+ +

First run

+
systemctl start maddy
+
+ +

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

+

DNS records

+

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

+
; 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 mx ~all"
+
+; Opt-in into DMARC with permissive policy and request reports about broken
+; messages.
+_dmarc.example.org.   TXT    "v=DMARC1; p=quarantine; ruf=mailto:postmaster@example.org"
+
+; Mark domain as MTA-STS compatible (see the next section)
+; and request reports about failures to be sent to postmaster@example.org
+_mta-sts.example.org.   TXT    "v=STSv1; id=1"
+_smtp._tls.example.org. TXT    "v=TLSRPTv1;rua=mailto:postmaster@example.org"
+
+ +

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.org
+
+ +

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

+
mx: mx1.example.org
+mx: mx2.example.org
+
+ +

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

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

User accounts and maddy command

+

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

+

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

+

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

+
$ maddy creds create postmaster@example.org
+
+ +

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 postmaster@example.org
+
+ +

That is it. Now you have your first e-mail address. when authenticating using +your e-mail client, do not forget the username is "postmaster@example.org", 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.

+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + + diff --git a/upgrading/index.html b/upgrading/index.html new file mode 100644 index 00000000..30c166fe --- /dev/null +++ b/upgrading/index.html @@ -0,0 +1,778 @@ + + + + + + + + +Upgrading from older maddy versions - maddy + + + +
+ +
+ +
+ +
+ +

Upgrading from older maddy versions

+

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

+

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

+

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

+

Incompatible version migration

+

0.2 -> 0.3

+

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

+

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

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

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
+    }
+}
+
+ +
    +
  1. +

    Use maddy creds create ACCOUNT_NAME to add credentials to pass_table + store.

    +
  2. +
  3. +

    Start the server back.

    +
  4. +
+
systemctl start maddy
+
+ +

0.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"
+
+
+ +
+ + + + + + + +
+ +
+
+ +
+ +
+
+Thanks Alberto Bertogli for providing base for this neat mkdocs theme! +Last updated: 2022-12-05 +
+
+ + + + + + + + +