first commit

This commit is contained in:
yann 2025-09-05 16:54:41 +02:00
commit 1910e01da1
433 changed files with 61737 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
testdata/
cmd/maddy/maddy
maddy
tests/maddy.cover

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{scd,go}]
indent_style = tab
indent_size = 4
[*.yml]
indent_style = tab
indent_size = 2

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

52
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@ -0,0 +1,52 @@
# Code of Merit
**1.** The project creators, lead developers, core team, constitute the managing
members of the project and have final say in every decision of the project,
technical or otherwise, including overruling previous decisions. There are no
limitations to this decisional power.
**2.** Contributions are an expected result of your membership on the project.
Dont expect others to do your work or help you with your work forever.
**3.** All members have the same opportunities to seek any challenge they want
within the project.
**4.** Authority or position in the project will be proportional to the accrued
contribution. Seniority must be earned.
**5.** Software is evolutive: the better implementations must supersede lesser
implementations. Technical advantage is the primary evaluation metric.
**6.** This is a space for technical prowess; topics outside of the project will
not be tolerated.
**7.** Non technical conflicts will be discussed in a separate space. Disruption
of the project will not be allowed.
**8.** Individual characteristics, including but not limited to, body, sex,
sexual preference, race, language, religion, nationality, or political
preferences are irrelevant in the scope of the project and will not be taken
into account concerning your value or that of your contribution to the project.
**9.** Discuss or debate the idea, not the person.
**10.** There is no room for ambiguity: Ambiguity will be met with questioning;
further ambiguity will be met with silence. It is the responsibility of the
originator to provide requested context.
**11.** If something is illegal outside the scope of the project, it is illegal
in the scope of the project. This Code of Merit does not take precedence over
governing law.
**12.** This Code of Merit governs the technical procedures of the project not
the activities outside of it.
**13.** Participation on the project equates to agreement of this Code of Merit.
**14.** No objectives beyond the stated objectives of this project are relevant
to the project. Any intent to deviate the project from its original purpose of
existence will constitute grounds for remedial action which may include
expulsion from the project.
This document is the Code of Merit
(<strike>`http://code-of-merit.org`</strike>), version 1.0.

59
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,59 @@
# Contributing Guidelines
Of course, we love our contributors. Thanks for spending time on making maddy
better.
## Reporting bugs
**Issue tracker is meant to be used only if you have a problem or a feature
request. If you just have some questions about maddy - prefer to use the
[IRC channel](https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1).**
- Provide log files, preferably with 'debug' directive set.
- Provide the exact steps to reproduce the issue.
- Provide the example message that causes the error, if applicable.
- "Too much information is better than not enough information".
Issues without enough information will be ignored and possibly closed.
Take some time to be more useful.
See SECURITY.md for information on how to report vulnerabilities.
## Contributing Code
0. Use common sense.
1. Learn Git. Especially, what is `git rebase`. We may ask you to use it if
needed.
2. Tell us that you are willing to work on an issue.
3. Fork the repo. Create a new branch based on `dev`, write your code. Open a
PR.
Ask for advice if you are not sure. We don't bite.
maddy design summary and some recommendations are provided in
[HACKING.md](../HACKING.md) file.
## Commits
1. Prefix commit message with a package path if it affects only a single
package. Omit `internal/` for brevity.
2. Provide reasoning for details in the source code itself (via comments),
provide reasoning for high-level decisions in the commit message.
3. Make sure every commit builds & passes tests. Otherwise `git bisect` becomes
unusable.
## Git workflow
`dev` branch includes the in-development version for the next feature release.
It is based on commit of the latest stable release and is merged into `master`
on release via fast-forward. Unlike `master`, `dev` **is not a protected branch
and may get force-pushes**.
`master` branch contains the latest stable release and is frozen between
releases.
`fix-X.Y` are temporary branches containing backported security fixes.
They are based on the commit of the corresponding stable release and exist
while the corresponding release is maintained. A `fix-*` branch is not created
for the latest release. Changes are added to these branches by cherry-picking
needed commits from the `dev` branch.

27
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,27 @@
---
name: Bug report
about: If you think something is broken
title: Bug report
labels: bug
assignees: ''
---
# Describe the bug
What do you think is wrong?
# Steps to reproduce
# Log files
Use a service like hastebin.com or attach a file if it is big
# Configuration file
Located in /etc/maddy/maddy.conf by default, don't forget to remove DB passwords
and other security-related stuff.
# Environment information
* maddy version: ?

7
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,7 @@
contact_links:
- name: Questions
url: "https://github.com/foxcpp/maddy/discussions/new?category=q-a"
about: "Use GitHub discussions for any questions"
- name: IRC channel
url: "https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1"
about: "... or there is also an IRC channel for any discussions"

View File

@ -0,0 +1,22 @@
---
name: Feature request
about: If you would like to see a new feature in maddy.
title: Feature request
labels: new feature
assignees: ''
---
# Use case
What problem you are trying to solve?
Note alternatives you considered and why they are not useful.
# Your idea for a solution
How your solution would work in general?
Note that some overly complicated solutions may be rejected because maddy is
meant to be simple.
- [ ] I'm willing to help with the implementation

17
.github/SECURITY.md vendored Normal file
View File

@ -0,0 +1,17 @@
# Security Policy
## Supported Versions
Two latest incompatible releases (e.g. 2.0.0 and 1.9.0).
Latest release gets all bug fixes, features, etc. Previous incompatible release
gets security fixes and fixes for problems that render software completely
unusable in certain configurations with no workaround.
## Reporting a Vulnerability
If you believe the vulnerabilitiy does have a big impact on existing
deployments - email `fox.cpp at disroot.org`, put "[maddy security]" in the
Subject.
Otherwise, open a public issue.

41
.github/releases.md vendored Normal file
View File

@ -0,0 +1,41 @@
# Release preparation
1. Run linters, fix all simple warnings. If the behavior is intentional - add
`nolint` comment and explanation. If the warning is non-trviail to fix - open
an issue.
```
golangci-lint run
```
2. Run unit tests suite. Verify that all disabled tests are not related to
serious problems and have corresponding issue open.
```
go test ./...
```
3. Run integration tests suite. Verify that all disabled tests are not related
to serious problems and have corresponding issue open.
```
cd tests/
./run.sh
```
4. Write release notes.
5. Create PGP-signed Git tag and push it to GitHub (do not create a "release"
yet).
5. Use environment configuration from maddy-repro bundle
(https://foxcpp.dev/maddy-repro) to build release artifacts.
6. Create detached PGP signatures for artifacts using key
3197BBD95137E682A59717B434BB2007081396F4.
7. Create sha256sums file for artifacts.
8. Create release on GitHub using the same text for
release notes. Attach signed artifacts and sha256sums file.
9. Build the Docker container and push it to hub.docker.com.
10. Post a message on the sr.ht mailing list.

158
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,158 @@
name: "Prepare release artifacts"
on:
push:
tags: [ "v*" ]
permissions:
id-token: write
contents: read
attestations: write
packages: write
jobs:
artifact-builder-x86:
name: "Prepare release artifacts (x86)"
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
container:
image: "alpine:edge"
steps:
- uses: actions/checkout@v1 # v2 does not work with containers
- name: "Install build dependencies"
run: |
apk add --no-cache gcc go zstd
- name: "Create and package build tree"
run: |
./build.sh --builddir ~/package-output/ --static build
ver=$(cat .version)
if [ "v$ver" != "${{github.ref_name}}" ]; then echo ".version does not match the Git tag"; exit 1; fi
mv ~/package-output/ ~/maddy-$ver-x86_64-linux-musl
cd ~
tar c ./maddy-$ver-x86_64-linux-musl | zstd > ~/maddy-x86_64-linux-musl.tar.zst
cd -
- name: "Save source tree"
run: |
rm -rf .git
ver=$(cat .version)
cp -r . ~/maddy-$ver-src
cd ~
tar c ./maddy-$ver-src | zstd > ~/maddy-src.tar.zst
cd -
- name: "Upload source tree"
uses: actions/upload-artifact@v4
with:
name: maddy-src.tar.zst
path: '~/maddy-src.tar.zst'
if-no-files-found: error
- name: "Upload binary tree"
uses: actions/upload-artifact@v4
with:
name: maddy-binary.tar.zst
path: '~/maddy-x86_64-linux-musl.tar.zst'
if-no-files-found: error
- name: "Generate artifact attestation"
uses: actions/attest-build-provenance@v2
with:
subject-path: '~/maddy-x86_64-linux-musl.tar.zst'
artifact-builder-arm:
name: "Prepare release artifacts (aarch64)"
if: github.ref_type == 'tag'
runs-on: ubuntu-22.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Building in a Docker container is a workaround for the issue of
# JavaScript-based GitHub Actions not being supported in Alpine
# containers on the Arm64 platform. Otherwise, we could completely reuse
# artifact-builder-x86 as a matrix job by running it on an Arm runner.
- name: Build in Docker container
run: |
# Create Dockerfile for the build
cat > Dockerfile << 'EOF'
FROM alpine:edge
RUN apk add --no-cache gcc go zstd musl-dev scdoc
WORKDIR /build
COPY . .
RUN ./build.sh --builddir /package-output/ --static build && \
ver=$(cat .version) && \
if [ "v$ver" != "${{github.ref_name}}" ]; then echo ".version does not match the Git tag"; exit 1; fi && \
mv /package-output/ /maddy-$ver-aarch64-linux-musl && \
cd / && \
tar c ./maddy-$ver-aarch64-linux-musl | zstd > /maddy-aarch64-linux-musl.tar.zst
EOF
# Build the image, create a temporary container and copy the artifact.
docker build -t maddy-builder .
container_id=$(docker create maddy-builder)
docker cp $container_id:/maddy-aarch64-linux-musl.tar.zst .
docker rm $container_id
- name: Upload binary tree
uses: actions/upload-artifact@v4
with:
name: maddy-binary-aarch64.tar.zst
path: maddy-aarch64-linux-musl.tar.zst
if-no-files-found: error
- name: "Generate artifact attestation"
uses: actions/attest-build-provenance@v2
with:
subject-path: 'maddy-aarch64-linux-musl.tar.zst'
docker-builder:
name: "Build & push Docker image"
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: "Set up QEMU"
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
- name: "Set up Docker Buildx"
id: buildx
uses: docker/setup-buildx-action@v3
- name: "Login to Docker Hub"
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
logout: false
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
with:
registry: "ghcr.io"
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
logout: false # https://news.ycombinator.com/item?id=28607735
- name: "Generate container metadata"
uses: docker/metadata-action@v5
id: meta
with:
images: |
foxcpp/maddy
ghcr.io/foxcpp/maddy
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
labels: |
org.opencontainers.image.title=Maddy Mail Server
org.opencontainers.image.documentation=https://maddy.email/docker/
org.opencontainers.image.url=https://maddy.email
- name: "Build and push"
uses: docker/build-push-action@v6
id: docker
with:
context: .
platforms: linux/amd64 #,linux/arm64 Temporary disabled due to SIGSEGV in gcc.
file: Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: "Generate container attestation"
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/foxcpp/maddy
subject-digest: ${{ steps.docker.outputs.digest }}
push-to-registry: true

75
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: "Testing"
on:
push:
branches: [ master, dev ]
tags: [ "v*" ]
pull_request:
branches: [ master, dev ]
permissions:
contents: read
pull-requests: read
checks: write
jobs:
golangci:
name: Lint
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: "Install libpam"
run: |
sudo apt-get update
sudo apt-get install -y libpam-dev
- uses: golangci/golangci-lint-action@v6
with:
version: v1.60
args: "--timeout=30m"
buildsh:
name: "Verify build.sh"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: "Install libpam"
run: |
sudo apt-get update
sudo apt-get install -y libpam-dev
- name: "Verify build.sh"
run: |
./build.sh
./build.sh --destdir destdir/ install
find destdir/
test:
name: "Build and test"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: "Install libpam"
run: |
sudo apt-get update
sudo apt-get install -y libpam-dev
- name: "Unit & module tests"
run: |
go test ./... -coverprofile=coverage.out -covermode=atomic
- name: "Integration tests"
run: |
cd tests/
./run.sh
- uses: codecov/codecov-action@v2
with:
files: ./coverage.out
flags: unit
- uses: codecov/codecov-action@v2
with:
files: ./tests/coverage.out
flags: integration

49
.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
# gitignore.io
*.o
*.a
*.so
_obj
_test
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.exe~
*.test
*.prof
**/.envrc
**/.DS_Store
# Tests coverage
*.out
# Compiled binaries
cmd/maddy/maddy
cmd/maddy-*-helper/maddy-*-helper
/maddy
# Man pages
docs/man/*.1
docs/man/*.5
# Certificates and private keys.
*.pem
*.crt
*.key
# Some directories that may be created during test-runs
# in repo directory.
cmd/maddy/*mtasts-cache
cmd/maddy/*queue
build/
tests/maddy.cover
tests/maddy
.idea/

17
.golangci.yml Normal file
View File

@ -0,0 +1,17 @@
linters:
enable:
- gosimple
- errcheck
- staticcheck
- ineffassign
- typecheck
- govet
- unused
- goimports
- prealloc
- unconvert
- misspell
- whitespace
- nakedret
- dogsled
- copyloopvar

84
.mkdocs.yml Normal file
View File

@ -0,0 +1,84 @@
site_name: maddy
repo_url: https://github.com/foxcpp/maddy
theme: alb
markdown_extensions:
- codehilite:
guess_lang: false
nav:
- faq.md
- Tutorials:
- tutorials/setting-up.md
- tutorials/building-from-source.md
- tutorials/alias-to-remote.md
- tutorials/pam.md
- Release builds: 'https://maddy.email/builds/'
- multiple-domains.md
- upgrading.md
- seclevels.md
- docker.md
- Reference manual:
- reference/modules.md
- reference/global-config.md
- reference/tls.md
- reference/tls-acme.md
- Endpoints configuration:
- reference/endpoints/imap.md
- reference/endpoints/smtp.md
- reference/endpoints/openmetrics.md
- IMAP storage:
- reference/storage/imap-filters.md
- reference/storage/imapsql.md
- Blob storage:
- reference/blob/fs.md
- reference/blob/s3.md
- reference/smtp-pipeline.md
- SMTP targets:
- reference/targets/queue.md
- reference/targets/remote.md
- reference/targets/smtp.md
- SMTP checks:
- reference/checks/actions.md
- reference/checks/dkim.md
- reference/checks/spf.md
- reference/checks/milter.md
- reference/checks/rspamd.md
- reference/checks/dnsbl.md
- reference/checks/command.md
- reference/checks/authorize_sender.md
- reference/checks/misc.md
- SMTP modifiers:
- reference/modifiers/dkim.md
- reference/modifiers/envelope.md
- Lookup tables (string translation):
- reference/table/static.md
- reference/table/regexp.md
- reference/table/file.md
- reference/table/sql_query.md
- reference/table/chain.md
- reference/table/email_localpart.md
- reference/table/email_with_domain.md
- reference/table/auth.md
- Authentication providers:
- reference/auth/pass_table.md
- reference/auth/pam.md
- reference/auth/shadow.md
- reference/auth/external.md
- reference/auth/ldap.md
- reference/auth/dovecot_sasl.md
- reference/auth/plain_separate.md
- reference/auth/netauth.md
- reference/config-syntax.md
- Integration with software:
- third-party/dovecot.md
- third-party/smtp-servers.md
- third-party/rspamd.md
- third-party/mailman3.md
- Internals:
- internals/specifications.md
- internals/unicode.md
- internals/quirks.md
- internals/sqlite.md

1
.version Normal file
View File

@ -0,0 +1 @@
0.8.1

674
COPYING Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM golang:1.23-alpine AS build-env
ARG ADDITIONAL_BUILD_TAGS=""
RUN set -ex && \
apk upgrade --no-cache --available && \
apk add --no-cache build-base
WORKDIR /maddy
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN mkdir -p /pkg/data && \
cp maddy.conf.docker /pkg/data/maddy.conf && \
./build.sh --builddir /tmp --destdir /pkg/ --tags "docker ${ADDITIONAL_BUILD_TAGS}" build install
FROM alpine:3.21.2
LABEL maintainer="fox.cpp@disroot.org"
LABEL org.opencontainers.image.source=https://github.com/foxcpp/maddy
RUN set -ex && \
apk upgrade --no-cache --available && \
apk --no-cache add ca-certificates
COPY --from=build-env /pkg/data/maddy.conf /data/maddy.conf
COPY --from=build-env /pkg/usr/local/bin/maddy /bin/
EXPOSE 25 143 993 587 465
VOLUME ["/data"]
ENTRYPOINT ["/bin/maddy", "-config", "/data/maddy.conf"]
CMD ["run"]

130
HACKING.md Normal file
View File

@ -0,0 +1,130 @@
## Design goals
- **Make it easy to deploy.**
Minimal configuration changes should be required to get a typical mail server
running. Though, it is important to avoid making guesses for a
"zero-configuration". A wrong guess is worse than no guess.
- **Provide 80% of needed components.**
E-mail has evolved into a huge mess. With a single package to do one thing, it
quickly turns into a maintenance nightmare. Put all stuff mail server
typically needs into a single package. Though, leave controversial or highly
opinionated stuff out, don't force people to do things our way
(see next point).
- **Interoperate with existing software.**
Implement (de-facto) standard protocols not only for clients but also for
various server-side helper software (content filters, etc).
- **Be secure but interoperable.**
Verify DKIM signatures by default, use DMRAC policies by default, etc. This
makes default setup as secure as possible while maintaining reasonable
interoperability. Though, users can configure maddy to be stricter.
- **Achieve flexibility through composability.**
Allow connecting components in arbitrary ways instead of restricting users to
predefined templates.
- **Use Go concurrency features to the full extent.**
Do as much I/O as possible in parallel to minimize latencies. It is silly to
not do so when it is possible.
## Design summary
Here is a summary of how things are organized in maddy in general. It explains
things from the developer perspective and is meant to be used as an
introduction by the new developers/contributors. It is recommended to read
user documentation to understand how things work from the user perspective as
well.
- User documentation: [maddy.conf(5)](docs/man/maddy.5.scd)
- Design rationale: [Comments on design (Wiki)][1]
There are components called "modules". They are represented by objects
implementing the module.Module interface. Each module gets its unique name.
The function used to create a module instance is saved with this name as a key
into the global map called "modules registry".
Whenever module needs another module for some functionality, it references it
using a configuration directive with a matcher that internally calls
`modconfig.ModuleFromNode`. That function looks up the module "constructor" in
the registry, calls it with corresponding arguments, checks whether the
returned module satisfies the needed interfaces and then initializes it.
Alternatively, if configuration uses &-syntax to reference existing
configuration block, `ModuleFromNode` simply looks it up in the global instances
registry. All modules defined the configuration as a separate top-level blocks
are created before main initialization and are placed in the instances registry
where they can be looked up as mentioned before.
Top-level defined module instances are initialized (`Init` method) lazily as
they are required by other modules. 'smtp' and 'imap' modules follow a special
initialization path, so they are always initialized directly.
## Error handling
Familiarize yourself with the `github.com/foxcpp/maddy/framework/exterrors`
package and make sure you have the following for returned errors:
- SMTP status information (smtp\_code, smtp\_enchcode, smtp\_msg fields)
- SMTP message text should contain a generic description of the error
condition without any details to prevent accidental disclosure of the
server configuration details.
- `Temporary() == true` for temporary errors (see `exterrors.WithTemporary`)
- Field that includes the module name
The easiest way to get all of these is to use `exterrors.SMTPError`.
Put the original error into the `Err` field, so it can be inspected using
`errors.Is`, `errors.Unwrap`, etc. Put the module name into `CheckName` or
`TargetName`. Add any additional context information using the `Misc` field.
Note, the SMTP status code overrides the result of `exterrors.IsTemporary()`
for that error object, so set it using `exterrors.SMTPCode` that uses
`IsTemporary` to select between two codes.
If the error you are wrapping contains details in its structure fields (like
`*net.OpError`) - copy these values into `Misc` map, put the underlying error
object (`net.OpError.Err`, for example) into the `Err` field.
Avoid using `Reason` unless you are sure you can provide the error message
better than the `Err.Error()` or `Err` is `nil`.
Do not attempt to add a SMTP status information for every single possible
error. Use `exterrors.WithFields` with basic information for errors you don't
expect. The SMTP client will get the "Internal server error" message and this
is generally the right thing to do on unexpected errors.
### Goroutines and panics
If you start any goroutines - make sure to catch panics to make sure severe
bugs will not bring the whole server down.
## Adding a check
"Check" is a module that inspects the message and flags it as spam or rejects
it altogether based on some condition.
The skeleton for the stateful check module can be found in
`internal/check/skeleton.go`. Throw it into a file in
`internal/check/check_name` directory and start ~~breaking~~ extending it.
If you don't need any per-message state, you can use `StatelessCheck` wrapper.
See `check/dns` directory for a working example.
Here are some guidelines to make sure your check works well:
- RTFM, docs will tell you about any caveats.
- Don't share any state _between_ messages, your code will be executed in
parallel.
- Use `github.com/foxcpp/maddy/check.FailAction` to select behavior on check
failures. See other checks for examples on how to use it.
- You can assume that order of check functions execution is as follows:
`CheckConnection`, `CheckSender`, `CheckRcpt`, `CheckBody`.
## Adding a modifier
"Modifier" is a module that can modify some parts of the message data.
Note, currently this is not possible to modify the body contents, only header
can be modified.
Structure of the modifier implementation is similar to the structure of check
implementation, check `modify/replace\_addr.go` for a working example.
[1]: https://github.com/foxcpp/maddy/wiki/Dev:-Comments-on-design

25
README.md Normal file
View File

@ -0,0 +1,25 @@
Maddy Mail Server
=====================
> Composable all-in-one mail server.
Maddy Mail Server implements all functionality required to run a e-mail
server. It can send messages via SMTP (works as MTA), accept messages via SMTP
(works as MX) and store messages while providing access to them via IMAP.
In addition to that it implements auxiliary protocols that are mandatory
to keep email reasonably secure (DKIM, SPF, DMARC, DANE, MTA-STS).
It replaces Postfix, Dovecot, OpenDKIM, OpenSPF, OpenDMARC and more with one
daemon with uniform configuration and minimal maintenance cost.
**Note:** IMAP storage is "beta". If you are looking for stable and
feature-packed implementation you may want to use Dovecot instead. maddy still
can handle message delivery business.
[![CI status](https://img.shields.io/github/actions/workflow/status/foxcpp/maddy/cicd.yml?style=flat-square)](https://github.com/foxcpp/maddy/actions/workflows/cicd.yml)
[![Issues tracker](https://img.shields.io/github/issues/foxcpp/maddy?style=flat-square)](https://github.com/foxcpp/maddy)
* [Setup tutorial](https://maddy.email/tutorials/setting-up/)
* [Documentation](https://maddy.email/)
* [IRC channel](https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1)
* [Mailing list](https://lists.sr.ht/~foxcpp/maddy)

201
build.sh Executable file
View File

@ -0,0 +1,201 @@
#!/bin/sh
destdir=/
builddir="$PWD/build"
prefix=/usr/local
version=
static=0
if [ "${GOFLAGS}" = "" ]; then
GOFLAGS="-trimpath" # set some flags to avoid passing "" to go
fi
print_help() {
cat >&2 <<EOF
Usage:
./build.sh [options] {build,install}
Script to build, package or install Maddy Mail Server.
Options:
-h, --help guess!
--builddir directory to build in (default: $builddir)
Options for ./build.sh build:
--static build static self-contained executables (musl-libc recommended)
--tags <tags> build tags to use
--version <version> version tag to embed into executables (default: auto-detect)
Additional flags for "go build" can be provided using GOFLAGS environment variable.
Options for ./build.sh install:
--prefix <path> installation prefix (default: $prefix)
--destdir <path> system root (default: $destdir)
EOF
}
while :; do
case "$1" in
-h|--help)
print_help
exit
;;
--builddir)
shift
builddir="$1"
;;
--prefix)
shift
prefix="$1"
;;
--destdir)
shift
destdir="$1"
;;
--version)
shift
version="$1"
;;
--static)
static=1
;;
--tags)
shift
tags="$1"
;;
--)
break
shift
;;
-?*)
echo "Unknown option: ${1}. See --help." >&2
exit 2
;;
*)
break
esac
shift
done
configdir="${destdir}etc/maddy"
if [ "$version" = "" ]; then
version=unknown
if [ -e .version ]; then
version="$(cat .version)"
fi
if [ -e .git ] && command -v git 2>/dev/null >/dev/null; then
version="${version}+$(git rev-parse --short HEAD)"
fi
fi
set -e
build_man_pages() {
set +e
if ! command -v scdoc >/dev/null 2>/dev/null; then
echo '-- [!] No scdoc utility found. Skipping man pages building.' >&2
set -e
return
fi
set -e
echo '-- Building man pages...' >&2
mkdir -p "${builddir}/man"
for f in ./docs/man/*.1.scd; do
scdoc < "$f" > "${builddir}/man/$(basename "$f" .scd)"
done
}
build() {
mkdir -p "${builddir}"
echo "-- Version: ${version}" >&2
if [ "$(go env CC)" = "" ]; then
echo '-- [!] No C compiler available. maddy will be built without SQLite3 support and default configuration will be unusable.' >&2
fi
if [ "$static" -eq 1 ]; then
echo "-- Building main server executable..." >&2
# This is literally impossible to specify this line of arguments as part of ${GOFLAGS}
# using only POSIX sh features (and even with Bash extensions I can't figure it out).
go build -trimpath -buildmode pie -tags "$tags osusergo netgo static_build" \
-ldflags "-extldflags '-fno-PIC -static' -X \"github.com/foxcpp/maddy.Version=${version}\"" \
-o "${builddir}/maddy" ${GOFLAGS} ./cmd/maddy
else
echo "-- Building main server executable..." >&2
go build -tags "$tags" -trimpath -ldflags="-X \"github.com/foxcpp/maddy.Version=${version}\"" -o "${builddir}/maddy" ${GOFLAGS} ./cmd/maddy
fi
build_man_pages
echo "-- Copying misc files..." >&2
mkdir -p "${builddir}/systemd"
cp dist/systemd/*.service "${builddir}/systemd/"
cp maddy.conf "${builddir}/maddy.conf"
}
install() {
echo "-- Installing built files..." >&2
command install -m 0755 -d "${destdir}/${prefix}/bin/"
command install -m 0755 "${builddir}/maddy" "${destdir}/${prefix}/bin/"
command ln -sf maddy "${destdir}/${prefix}/bin/maddyctl"
command install -m 0755 -d "${configdir}"
# We do not want to overwrite existing configuration.
# If the file exists, then save it with .default suffix and warn user.
if [ ! -e "${configdir}/maddy.conf" ]; then
command install -m 0644 ./maddy.conf "${configdir}/maddy.conf"
else
echo "-- [!] Configuration file ${configdir}/maddy.conf exists, saving to ${configdir}/maddy.conf.default" >&2
command install -m 0644 ./maddy.conf "${configdir}/maddy.conf.default"
fi
# Attempt to install systemd units only for Linux.
# Check is done using GOOS instead of uname -s to account for possible
# package cross-compilation.
# Though go command might be unavailable if build.sh is run
# with sudo and go installation is user-specific, so fallback
# to using uname -s in the end.
set +e
if command -v go >/dev/null 2>/dev/null; then
set -e
if [ "$(go env GOOS)" = "linux" ]; then
command install -m 0755 -d "${destdir}/${prefix}/lib/systemd/system/"
command install -m 0644 "${builddir}"/systemd/*.service "${destdir}/${prefix}/lib/systemd/system/"
fi
else
set -e
if [ "$(uname -s)" = "Linux" ]; then
command install -m 0755 -d "${destdir}/${prefix}/lib/systemd/system/"
command install -m 0644 "${builddir}"/systemd/*.service "${destdir}/${prefix}/lib/systemd/system/"
fi
fi
if [ -e "${builddir}"/man ]; then
command install -m 0755 -d "${destdir}/${prefix}/share/man/man1/"
for f in "${builddir}"/man/*.1; do
command install -m 0644 "$f" "${destdir}/${prefix}/share/man/man1/"
done
fi
}
# Old build.sh compatibility
install_pkg() {
echo "-- [!] Replace 'install_pkg' with 'install' in build.sh invocation" >&2
install
}
package() {
echo "-- [!] Replace 'package' with 'build' in build.sh invocation" >&2
build
}
if [ $# -eq 0 ]; then
build
else
for arg in "$@"; do
eval "$arg"
done
fi

11
cmd/README.md Normal file
View File

@ -0,0 +1,11 @@
maddy executables
-------------------
### maddy
Main server executable.
### maddy-pam-helper, maddy-shadow-helper
Utilities compatible with the auth.external module that call libpam or read
/etc/shadow on Unix systems.

View File

@ -0,0 +1,65 @@
## maddy-pam-helper
External setuid binary for interaction with shadow passwords database or other
privileged objects necessary to run PAM authentication.
### Building
It is really easy to build it using any GCC:
```
gcc pam.c main.c -lpam -o maddy-pam-helper
```
Yes, it is not a Go binary.
### Installation
maddy-pam-helper is kinda dangerous binary and should not be allowed to be
executed by everybody but maddy's user. At the same moment it needs to have
access to read-protected files. For this reason installation should be done
very carefully to make sure to not introduce any security "holes".
#### First method
```shell
chown maddy: /usr/bin/maddy-pam-helper
chmod u+x,g-x,o-x /usr/bin/maddy-pam-helper
```
Also maddy-pam-helper needs access to /etc/shadow, one of the ways to provide
it is to set file capability CAP_DAC_READ_SEARCH:
```
setcap cap_dac_read_search+ep /usr/bin/maddy-pam-helper
```
#### Second method
Another, less restrictive is to make it setuid-root (assuming you have both maddy user and group):
```
chown root:maddy /usr/bin/maddy-pam-helper
chmod u+xs,g+x,o-x /usr/bin/maddy-pam-helper
```
#### Third method
The best way actually is to create `shadow` group and grant access to
/etc/shadow to it and then make maddy-pam-helper setgid-shadow:
```
groupadd shadow
chown :shadow /etc/shadow
chmod g+r /etc/shadow
chown maddy:shadow /usr/bin/maddy-pam-helper
chmod u+x,g+xs /usr/bin/maddy-pam-helper
```
Pick what works best for you.
### PAM service
maddy-pam-helper uses custom service instead of pretending to be su or sudo.
Because of this you should configure PAM to accept it.
Minimal example using local passwd/shadow database for authentication can be
found in [maddy.conf][maddy.conf] file.
It should be put into /etc/pam.d/maddy.

View File

@ -0,0 +1,3 @@
#%PAM-1.0
auth required pam_unix.so
account required pam_unix.so

View File

@ -0,0 +1,51 @@
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <security/pam_appl.h>
#include "pam.h"
/*
I really doubt it is a good idea to bring Go to the binary whose primary task
is to call libpam using CGo anyway.
*/
int run(void) {
char *username = NULL, *password = NULL;
size_t username_buf_len = 0, password_buf_len = 0;
ssize_t username_len = getline(&username, &username_buf_len, stdin);
if (username_len < 0) {
perror("getline username");
return 2;
}
ssize_t password_len = getline(&password, &password_buf_len, stdin);
if (password_len < 0) {
perror("getline password");
return 2;
}
// Cut trailing \n.
if (username_len > 0) {
username[username_len - 1] = 0;
}
if (password_len > 0) {
password[password_len - 1] = 0;
}
struct error_obj err = run_pam_auth(username, password);
if (err.status != 0) {
if (err.status == 2) {
fprintf(stderr, "%s: %s\n", err.func_name, err.error_msg);
}
return err.status;
}
return 0;
}
#ifndef CGO
int main() {
return run();
}
#endif

View File

@ -0,0 +1,38 @@
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
/*
#cgo LDFLAGS: -lpam
#cgo CFLAGS: -DCGO -Wall -Wextra -Werror -Wno-unused-parameter -Wno-error=unused-parameter -Wpedantic -std=c99
extern int run();
*/
import "C"
import "os"
/*
Apparently, some people would not want to build it manually by calling GCC.
Here we do it for them. Not going to tell them that resulting file is 800KiB
bigger than one built using only C compiler.
*/
func main() {
i := int(C.run())
os.Exit(i)
}

100
cmd/maddy-pam-helper/pam.c Normal file
View File

@ -0,0 +1,100 @@
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2022 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <security/pam_appl.h>
#include "pam.h"
static int conv_func(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) {
struct pam_response *reply = malloc(sizeof(struct pam_response));
if (reply == NULL) {
return PAM_CONV_ERR;
}
char* password_cpy = malloc(strlen((char*)appdata_ptr)+1);
if (password_cpy == NULL) {
return PAM_CONV_ERR;
}
memcpy(password_cpy, (char*)appdata_ptr, strlen((char*)appdata_ptr)+1);
reply->resp = password_cpy;
reply->resp_retcode = 0;
// PAM frees pam_response for us.
*resp = reply;
return PAM_SUCCESS;
}
struct error_obj run_pam_auth(const char *username, char *password) {
const struct pam_conv local_conv = { conv_func, password };
pam_handle_t *local_auth = NULL;
int status = pam_start("maddy", username, &local_conv, &local_auth);
if (status != PAM_SUCCESS) {
struct error_obj ret_val;
ret_val.status = 2;
ret_val.func_name = "pam_start";
ret_val.error_msg = pam_strerror(local_auth, status);
return ret_val;
}
status = pam_authenticate(local_auth, PAM_SILENT|PAM_DISALLOW_NULL_AUTHTOK);
if (status != PAM_SUCCESS) {
struct error_obj ret_val;
if (status == PAM_AUTH_ERR || status == PAM_USER_UNKNOWN) {
ret_val.status = 1;
} else {
ret_val.status = 2;
}
ret_val.func_name = "pam_authenticate";
ret_val.error_msg = pam_strerror(local_auth, status);
return ret_val;
}
status = pam_acct_mgmt(local_auth, PAM_SILENT|PAM_DISALLOW_NULL_AUTHTOK);
if (status != PAM_SUCCESS) {
struct error_obj ret_val;
if (status == PAM_AUTH_ERR || status == PAM_USER_UNKNOWN || status == PAM_NEW_AUTHTOK_REQD) {
ret_val.status = 1;
} else {
ret_val.status = 2;
}
ret_val.func_name = "pam_acct_mgmt";
ret_val.error_msg = pam_strerror(local_auth, status);
return ret_val;
}
status = pam_end(local_auth, status);
if (status != PAM_SUCCESS) {
struct error_obj ret_val;
ret_val.status = 2;
ret_val.func_name = "pam_end";
ret_val.error_msg = pam_strerror(local_auth, status);
return ret_val;
}
struct error_obj ret_val;
ret_val.status = 0;
ret_val.func_name = NULL;
ret_val.error_msg = NULL;
return ret_val;
}

View File

@ -0,0 +1,27 @@
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
struct error_obj {
int status;
const char* func_name;
const char* error_msg;
};
struct error_obj run_pam_auth(const char *username, char *password);

View File

@ -0,0 +1,47 @@
## maddy-shadow-helper
External helper binary for interaction with shadow passwords database.
Unlike maddy-pam-helper it supports only local shadow database but it does
not have any C dependencies.
### Installation
maddy-shadow-helper is kinda dangerous binary and should not be allowed to be
executed by everybody but maddy's user. At the same moment it needs to have
access to read-protected files. For this reason installation should be done
very carefully to make sure to not introduce any security "holes".
#### First method
```shell
chown maddy: /usr/bin/maddy-shadow-helper
chmod u+x,g-x,o-x /usr/bin/maddy-shadow-helper
```
Also maddy-shadow-helper needs access to /etc/shadow, one of the ways to provide
it is to set file capability CAP_DAC_READ_SEARCH:
```
setcap cap_dac_read_search+ep /usr/bin/maddy-shadow-helper
```
#### Second method
Another, less restrictive is to make it setuid-root (assuming you have both maddy user and group):
```
chown root:maddy /usr/bin/maddy-shadow-helper
chmod u+xs,g+x,o-x /usr/bin/maddy-shadow-helper
```
#### Third method
The best way actually is to create `shadow` group and grant access to
/etc/shadow to it and then make maddy-shadow-helper setgid-shadow:
```
groupadd shadow
chown :shadow /etc/shadow
chmod g+r /etc/shadow
chown maddy:shadow /usr/bin/maddy-shadow-helper
chmod u+x,g+xs /usr/bin/maddy-shadow-helper
```
Pick what works best for you.

View File

@ -0,0 +1,71 @@
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"bufio"
"errors"
"fmt"
"os"
"github.com/foxcpp/maddy/internal/auth/shadow"
)
func main() {
scnr := bufio.NewScanner(os.Stdin)
if !scnr.Scan() {
fmt.Fprintln(os.Stderr, scnr.Err())
os.Exit(2)
}
username := scnr.Text()
if !scnr.Scan() {
fmt.Fprintln(os.Stderr, scnr.Err())
os.Exit(2)
}
password := scnr.Text()
ent, err := shadow.Lookup(username)
if err != nil {
if errors.Is(err, shadow.ErrNoSuchUser) {
os.Exit(1)
}
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if !ent.IsAccountValid() {
fmt.Fprintln(os.Stderr, "account is expired")
os.Exit(1)
}
if !ent.IsPasswordValid() {
fmt.Fprintln(os.Stderr, "password is expired")
os.Exit(1)
}
if err := ent.VerifyPassword(password); err != nil {
if errors.Is(err, shadow.ErrWrongPassword) {
os.Exit(1)
}
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

29
cmd/maddy/main.go Normal file
View File

@ -0,0 +1,29 @@
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
_ "github.com/foxcpp/maddy"
maddycli "github.com/foxcpp/maddy/internal/cli"
_ "github.com/foxcpp/maddy/internal/cli/ctl"
)
func main() {
maddycli.Run()
}

120
config.go Normal file
View File

@ -0,0 +1,120 @@
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package maddy
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/foxcpp/maddy/framework/config"
"github.com/foxcpp/maddy/framework/log"
)
/*
Config matchers for module interfaces.
*/
// logOut structure wraps log.Output and preserves
// configuration directive it was constructed from, allowing
// dynamic reinitialization for purposes of log file rotation.
type logOut struct {
args []string
log.Output
}
func logOutput(_ *config.Map, node config.Node) (interface{}, error) {
if len(node.Args) == 0 {
return nil, config.NodeErr(node, "expected at least 1 argument")
}
if len(node.Children) != 0 {
return nil, config.NodeErr(node, "can't declare block here")
}
return LogOutputOption(node.Args)
}
func LogOutputOption(args []string) (log.Output, error) {
outs := make([]log.Output, 0, len(args))
for i, arg := range args {
switch arg {
case "stderr":
outs = append(outs, log.WriterOutput(os.Stderr, false))
case "stderr_ts":
outs = append(outs, log.WriterOutput(os.Stderr, true))
case "syslog":
syslogOut, err := log.SyslogOutput()
if err != nil {
return nil, fmt.Errorf("failed to connect to syslog daemon: %v", err)
}
outs = append(outs, syslogOut)
case "off":
if len(args) != 1 {
return nil, errors.New("'off' can't be combined with other log targets")
}
return log.NopOutput{}, nil
default:
// Log file paths are converted to absolute to make sure
// we will be able to recreate them in right location
// after changing working directory to the state dir.
absPath, err := filepath.Abs(arg)
if err != nil {
return nil, err
}
// We change the actual argument, so logOut object will
// keep the absolute path for reinitialization.
args[i] = absPath
w, err := os.OpenFile(absPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666)
if err != nil {
return nil, fmt.Errorf("failed to create log file: %v", err)
}
outs = append(outs, log.WriteCloserOutput(w, true))
}
}
if len(outs) == 1 {
return logOut{args, outs[0]}, nil
}
return logOut{args, log.MultiOutput(outs...)}, nil
}
func defaultLogOutput() (interface{}, error) {
return log.DefaultLogger.Out, nil
}
func reinitLogging() {
out, ok := log.DefaultLogger.Out.(logOut)
if !ok {
log.Println("Can't reinitialize logger because it was replaced before, this is a bug")
return
}
newOut, err := LogOutputOption(out.args)
if err != nil {
log.Println("Can't reinitialize logger:", err)
return
}
out.Close()
log.DefaultLogger.Out = newOut
}

6
contrib/README.md Normal file
View File

@ -0,0 +1,6 @@
# Community contributed resources
Disclaimer: Nothing inside subdirectories here is directly supported by Maddy
Mail Server maintainers. Some community members may be able to help you or not.
- Kubernetes helm chart is maintained by @acim.

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -0,0 +1,23 @@
apiVersion: v2
name: maddy
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.2.6
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 0.4.0

View File

@ -0,0 +1,69 @@
# maddy Helm chart for Kubernetes
![Version: 0.2.5](https://img.shields.io/badge/Version-0.2.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.4.1](https://img.shields.io/badge/AppVersion-0.4.1-informational?style=flat-square)
This is just initial effort to run maddy within Kubernetes cluster. We have used Deployment resource which has some downsides
but at least this chart will allow you to install maddy relatively easily on your Kubernetes cluster. We have considered
StatefulSet and DaemonSet but such solutions would require much more configuration and in casae of DaemonSet also a TCP
load balancer in front of the nodes.
## Requirement
In order to run maddy properly, you need to have TLS secret under name maddy present in the cluster. If you have commercial
certificate, you can create it by the following command:
```sh
kubectl create secret tls maddy --cert=fullchain.pem --key=privkey.pem
```
If you use cert-manager, just create the secret under name maddy.
## Replication
Default for this chart is 1 replica of maddy. If you try to increase this, you will probably get an error because of
the busy ports 25, 143, 587, etc. We do not support this feature at the moment, so please use just 1 replica. Like said
at the beginning of this document, multiple replicas would probably require to switch do DaemonSet which would further require
to have TCP load balancer and shared storage between all replicas. This is not supported by this chart, sorry.
This chart is used on one node cluster and then installation is straight forward, like described bellow, but if you have
multiple node cluster, please use taints and tolerations to select the desired node. This chart supports tolerations to
be set.
## Configuration
| Key | Type | Default | Description |
| -------------------------- | ------ | ----------------- | ----------- |
| affinity | object | `{}` | |
| fullnameOverride | string | `""` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"foxcpp/maddy"` | |
| image.tag | string | `""` | |
| imagePullSecrets | list | `[]` | |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| persistence.accessMode | string | `"ReadWriteOnce"` | |
| persistence.annotations | object | `{}` | |
| persistence.enabled | bool | `false` | |
| persistence.path | string | `"/data"` | |
| persistence.size | string | `"128Mi"` | |
| podAnnotations | object | `{}` | |
| podSecurityContext | object | `{}` | |
| replicaCount | int | `1` | |
| resources | object | `{}` | |
| securityContext | object | `{}` | |
| service.type | string | `"NodePort"` | |
| serviceAccount.annotations | object | `{}` | |
| serviceAccount.create | bool | `true` | |
| serviceAccount.name | string | `""` | |
| tolerations | list | `[]` | |
## Installing the chart
```sh
helm upgrade --install maddy ./chart --set service.externapIPs[0]=1.2.3.4
```
1.2.3.4 is your public IP of the node.
## maddy configuration
Feel free to tweak files/maddy.conf and files/aliases according to your needs.

View File

@ -0,0 +1 @@
info@example.org: foxcpp@example.org

View File

@ -0,0 +1,171 @@
## maddy 0.3 - default configuration file (2020-05-31)
# Suitable for small-scale deployments. Uses its own format for local users DB,
# should be managed via maddy subcommands.
#
# See tutorials at https://foxcpp.dev/maddy for guidance on typical
# configuration changes.
#
# See manual pages (also available at https://foxcpp.dev/maddy) for reference
# documentation.
# ----------------------------------------------------------------------------
# Base variables
$(hostname) = mx1.example.org
$(primary_domain) = example.org
$(local_domains) = $(primary_domain)
tls file /etc/maddy/certs/fullchain.pem /etc/maddy/certs/privkey.pem
# ----------------------------------------------------------------------------
# Local storage & authentication
# pass_table provides local hashed passwords storage for authentication of
# users. It can be configured to use any "table" module, in default
# configuration a table in SQLite DB is used.
# Table can be replaced to use e.g. a file for passwords. Or pass_table module
# can be replaced altogether to use some external source of credentials (e.g.
# PAM, /etc/shadow file).
#
# If table module supports it (sql_table does) - credentials can be managed
# using 'maddy creds' command.
auth.pass_table local_authdb {
table sql_table {
driver sqlite3
dsn credentials.db
table_name passwords
}
}
# imapsql module stores all indexes and metadata necessary for IMAP using a
# relational database. It is used by IMAP endpoint for mailbox access and
# also by SMTP & Submission endpoints for delivery of local messages.
#
# IMAP accounts, mailboxes and all message metadata can be inspected using
# imap-* subcommands of maddy.
storage.imapsql local_mailboxes {
driver sqlite3
dsn imapsql.db
}
# ----------------------------------------------------------------------------
# SMTP endpoints + message routing
hostname $(hostname)
msgpipeline local_routing {
dmarc yes
check {
require_matching_ehlo
require_mx_record
dkim
spf
}
# Insert handling for special-purpose local domains here.
# e.g.
# destination lists.example.org {
# deliver_to lmtp tcp://127.0.0.1:8024
# }
destination postmaster $(local_domains) {
modify {
replace_rcpt regexp "(.+)\+(.+)@(.+)" "$1@$3"
replace_rcpt file /data/aliases
}
deliver_to &local_mailboxes
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
smtp tcp://0.0.0.0:25 {
limits {
# Up to 20 msgs/sec across max. 10 SMTP connections.
all rate 20 1s
all concurrency 10
}
source $(local_domains) {
reject 501 5.1.8 "Use Submission for outgoing SMTP"
}
default_source {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
}
submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
limits {
# Up to 50 msgs/sec across any amount of SMTP connections.
all rate 50 1s
}
auth &local_authdb
source $(local_domains) {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
modify {
dkim $(primary_domain) $(local_domains) default
}
deliver_to &remote_queue
}
}
default_source {
reject 501 5.1.8 "Non-local sender domain"
}
}
target.remote outbound_delivery {
limits {
# Up to 20 msgs/sec across max. 10 SMTP connections
# for each recipient domain.
destination rate 20 1s
destination concurrency 10
}
mx_auth {
dane
mtasts {
cache fs
fs_dir mtasts_cache/
}
local_policy {
min_tls_level encrypted
min_mx_level none
}
}
}
target.queue remote_queue {
target &outbound_delivery
autogenerated_msg_domain $(primary_domain)
bounce {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
}
}
}
# ----------------------------------------------------------------------------
# IMAP endpoints
imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
auth &local_authdb
storage &local_mailboxes
}

View File

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "maddy.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "maddy.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "maddy.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "maddy.labels" -}}
helm.sh/chart: {{ include "maddy.chart" . }}
{{ include "maddy.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "maddy.selectorLabels" -}}
app.kubernetes.io/name: {{ include "maddy.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "maddy.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "maddy.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{include "maddy.fullname" .}}
labels: {{- include "maddy.labels" . | nindent 4}}
data:
maddy.conf: |
{{ tpl (.Files.Get "files/maddy.conf") . | printf "%s" | indent 4 }}
aliases: |
{{ tpl (.Files.Get "files/aliases") . | printf "%s" | indent 4 }}

View File

@ -0,0 +1,113 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "maddy.fullname" . }}
labels:
{{- include "maddy.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "maddy.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ tpl (.Files.Get "files/maddy.conf") . | printf "%s" | sha256sum }}
checksum/aliases: {{ tpl (.Files.Get "files/aliases") . | printf "%s" | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "maddy.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "maddy.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
initContainers:
- name: init
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: busybox
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- sh
- -c
- cp /tmp/maddy/* /data/.
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: {{ .Values.persistence.path }}
{{- if .Values.persistence.subPath }}
subPath: {{ .Values.persistence.subPath }}
{{- end }}
- name: config
mountPath: /tmp/maddy
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: smtp
containerPort: 25
protocol: TCP
- name: imaps
containerPort: 993
protocol: TCP
- name: starttls
containerPort: 587
protocol: TCP
# livenessProbe:
# httpGet:
# path: /
# port: http
# readinessProbe:
# httpGet:
# path: /
# port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: {{ .Values.persistence.path }}
{{- if .Values.persistence.subPath }}
subPath: {{ .Values.persistence.subPath }}
{{- end }}
- name: tls
mountPath: /etc/maddy/certs/fullchain.pem
subPath: tls.crt
- name: tls
mountPath: /etc/maddy/certs/privkey.pem
subPath: tls.key
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ default (include "maddy.fullname" .) .Values.persistence.existingClaim }}
{{- else }}
emptyDir: {}
{{- end }}
- name: config
configMap:
name: {{include "maddy.fullname" .}}
- name: tls
secret:
secretName: {{include "maddy.fullname" .}}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,22 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "maddy.fullname" . }}
annotations:
{{- with .Values.persistence.annotations }}
{{ toYaml . | indent 4 }}
{{- end }}
labels:
{{- include "maddy.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- end -}}

View File

@ -0,0 +1,27 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "maddy.fullname" . }}
labels:
{{- include "maddy.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: 25
targetPort: smtp
protocol: TCP
name: smtp
- port: 993
targetPort: imaps
protocol: TCP
name: imaps
- port: 587
targetPort: starttls
protocol: TCP
name: starttls
selector:
{{- include "maddy.selectorLabels" . | nindent 4 }}
{{- with .Values.service.externalIPs }}
externalIPs:
{{- toYaml . | nindent 6 }}
{{- end -}}

View File

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "maddy.serviceAccountName" . }}
labels:
{{- include "maddy.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "maddy.fullname" . }}-test-connection"
labels:
{{- include "maddy.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "maddy.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@ -0,0 +1,74 @@
# Default values for maddy.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1 # Multiple replicas are not supported, please don't change this.
image:
repository: foxcpp/maddy
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext:
{}
# fsGroup: 2000
securityContext:
{}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
# Set externalPIs to your public IP(s) of the node running maddy. In case of multiple nodes, you need to set tolerations
# and taints in order to run maddy on the exact node.
service:
type: NodePort
# externalIPs:
resources:
{}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
persistence:
enabled: false
# existingClaim: ""
accessMode: ReadWriteOnce
size: 128Mi
# storageClass: ""
path: /data
annotations: {}
# subPath: "" # only mount a subpath of the Volume into the pod
nodeSelector: {}
tolerations: []
affinity: {}

46
directories.go Normal file
View File

@ -0,0 +1,46 @@
//go:build !docker
// +build !docker
package maddy
var (
// ConfigDirectory specifies platform-specific value
// that should be used as a location of default configuration
//
// It should not be changed and is defined as a variable
// only for purposes of modification using -X linker flag.
ConfigDirectory = "/etc/maddy"
// DefaultStateDirectory specifies platform-specific
// default for StateDirectory.
//
// Most code should use StateDirectory instead since
// it will contain the effective location of the state
// directory.
//
// It should not be changed and is defined as a variable
// only for purposes of modification using -X linker flag.
DefaultStateDirectory = "/var/lib/maddy"
// DefaultRuntimeDirectory specifies platform-specific
// default for RuntimeDirectory.
//
// Most code should use RuntimeDirectory instead since
// it will contain the effective location of the state
// directory.
//
// It should not be changed and is defined as a variable
// only for purposes of modification using -X linker flag.
DefaultRuntimeDirectory = "/run/maddy"
// DefaultLibexecDirectory specifies platform-specific
// default for LibexecDirectory.
//
// Most code should use LibexecDirectory since it will
// contain the effective location of the libexec
// directory.
//
// It should not be changed and is defined as a variable
// only for purposes of modification using -X linker flag.
DefaultLibexecDirectory = "/usr/lib/maddy"
)

11
directories_docker.go Normal file
View File

@ -0,0 +1,11 @@
//go:build docker
// +build docker
package maddy
var (
ConfigDirectory = "/data"
DefaultStateDirectory = "/data"
DefaultRuntimeDirectory = "/tmp"
DefaultLibexecDirectory = "/usr/lib/maddy"
)

41
dist/README.md vendored Normal file
View File

@ -0,0 +1,41 @@
Distribution files for maddy
------------------------------
**Disclaimer:** Most of the files here are maintained in a "best-effort" way.
That is, they may break or become outdated from time to time. Caveat emptor.
## integration + scripts
These directories provide pre-made configuration snippets suitable for
easy integration with external software.
Usually, this is what you use when you put `import integration/something` in
your config.
## systemd unit
`maddy.service` launches using default config path (/etc/maddy/maddy.conf).
`maddy@.service` launches maddy using custom config path. E.g.
`maddy@foo.service` will use /etc/maddy/foo.conf.
Additionally, unit files apply strict sandboxing, limiting maddy permissions on
the system to a bare minimum. Subset of these options makes it impossible for
privileged authentication helper binaries to gain required permissions, so you
may have to disable it when using system account-based authentication with
maddy running as a unprivileged user.
## fail2ban configuration
Configuration files for use with fail2ban. Assume either `backend = systemd` specified
in system-wide configuration or log file written to /var/log/maddy/maddy.log.
See https://github.com/foxcpp/maddy/wiki/fail2ban-configuration for details.
## logrotate configuration
Meant for logs rotation when logging to file is used.
## vim ftdetect/ftplugin/syntax files
Minimal supplement to make configuration files more readable and help you see
typos in directive names.

38
dist/apparmor/dev.foxcpp.maddy vendored Normal file
View File

@ -0,0 +1,38 @@
# AppArmor profile for maddy daemon.
# vim:syntax=apparmor:ts=2:sw=2:et
#include <tunables/global>
profile dev.foxcpp.maddy /usr{/local,}/bin/maddy {
#include <abstractions/base>
#include <abstractions/ssl_certs>
#include <abstractions/ssl_keys>
/etc/ca-certificates/** r,
/etc/resolv.conf r,
/proc/sys/net/core/somaxconn r,
/sys/kernel/mm/transparent_hugepage/hpage_pmd_size r,
deny ptrace,
capability net_bind_service,
network tcp,
network unix,
# systemd process management and Type=notify
signal (receive) peer=unconfined,
signal (receive) peer=/usr/bin/systemd,
unix (create, connect, send, setopt) type=dgram addr=@*,
/run/systemd/notify w,
/etc/maddy/** r,
owner /run/maddy/ rw,
owner /run/maddy/** rwkl,
owner /var/lib/maddy/ rw,
owner /var/lib/maddy/** rwk,
owner /var/lib/maddy/**.db-{wal,shm} rmk,
/usr{/local,}/lib/maddy/* PUx,
/usr{/local,}/bin/maddy{,ctl} rmix,
#include if exists <local/dev.foxcpp.maddy>
}

View File

@ -0,0 +1,6 @@
[INCLUDES]
before = common.conf
[Definition]
failregex = authentication failed\t\{\"reason\":\".*\",\"src_ip\"\:\"<HOST>:\d+\"\,\"username\"\:\".*\"\}$
journalmatch = _SYSTEMD_UNIT=maddy.service + _COMM=maddy

View File

@ -0,0 +1,7 @@
[INCLUDES]
before = common.conf
[Definition]
failregex = smtp\: MAIL FROM error repeated a lot\, possible dictonary attack\t\{\"count\"\:\d+,\"msg_id\":\".+\",\"src_ip\"\:\"<HOST>:\d+\"\}$
smtp\: too many RCPT errors\, possible dictonary attack\t\{\"msg_id\":\".+\","src_ip":"<HOST>:\d+\"\}
journalmatch = _SYSTEMD_UNIT=maddy.service + _COMM=maddy

5
dist/fail2ban/jail.d/maddy-auth.conf vendored Normal file
View File

@ -0,0 +1,5 @@
[maddy-auth]
port = 993,465,25
filter = maddy-auth
bantime = 96h
backend = systemd

View File

@ -0,0 +1,7 @@
[maddy-dictonary-attack]
port = 993,465,25
filter = maddy-dictonary-attack
bantime = 72h
maxretry = 3
findtime = 6h
backend = systemd

24
dist/install.sh vendored Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
DESTDIR=$DESTDIR
if [ -z "$PREFIX" ]; then
PREFIX=/usr/local
fi
if [ -z "$FAIL2BANDIR" ]; then
FAIL2BANDIR=/etc/fail2ban
fi
if [ -z "$CONFDIR" ]; then
CONFDIR=/etc/maddy
fi
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $script_dir
install -Dm 0644 -t "$DESTDIR/$PREFIX/share/vim/vimfiles/ftdetect/" vim/ftdetect/maddy-conf.vim
install -Dm 0644 -t "$DESTDIR/$PREFIX/share/vim/vimfiles/ftplugin/" vim/ftplugin/maddy-conf.vim
install -Dm 0644 -t "$DESTDIR/$PREFIX/share/vim/vimfiles/syntax/" vim/syntax/maddy-conf.vim
install -Dm 0644 -t "$DESTDIR/$FAIL2BANDIR/jail.d/" fail2ban/jail.d/*
install -Dm 0644 -t "$DESTDIR/$FAIL2BANDIR/filter.d/" fail2ban/filter.d/*
install -Dm 0644 -t "$DESTDIR/$PREFIX/lib/systemd/system/" systemd/maddy.service systemd/maddy@.service

7
dist/logrotate.d/maddy vendored Normal file
View File

@ -0,0 +1,7 @@
/var/log/maddy/maddy.log {
missingok
su maddy maddy
postrotate
/usr/bin/killall -USR1 maddy
endscript
}

82
dist/systemd/maddy.service vendored Normal file
View File

@ -0,0 +1,82 @@
[Unit]
Description=maddy mail server
Documentation=man:maddy(1)
Documentation=man:maddy.conf(5)
Documentation=https://maddy.email
After=network-online.target
[Service]
Type=notify
NotifyAccess=main
User=maddy
Group=maddy
# cd to state directory to make sure any relative paths
# in config will be relative to it unless handled specially.
WorkingDirectory=/var/lib/maddy
ConfigurationDirectory=maddy
RuntimeDirectory=maddy
StateDirectory=maddy
LogsDirectory=maddy
ReadOnlyPaths=/usr/lib/maddy
ReadWritePaths=/var/lib/maddy
# Strict sandboxing. You have no reason to trust code written by strangers from GitHub.
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectHostname=true
ProtectClock=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# Additional sandboxing. You need to disable all of these options
# for privileged helper binaries (for system auth) to work correctly.
NoNewPrivileges=true
PrivateDevices=true
DeviceAllow=/dev/syslog
RestrictSUIDSGID=true
ProtectKernelModules=true
MemoryDenyWriteExecute=true
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true
# Graceful shutdown with a reasonable timeout.
TimeoutStopSec=7s
KillMode=mixed
KillSignal=SIGTERM
# Required to bind on ports lower than 1024.
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# Force all files created by maddy to be only readable by it
# and maddy group.
UMask=0007
# Bump FD limitations. Even idle mail server can have a lot of FDs open (think
# of idle IMAP connections, especially ones abandoned on the other end and
# slowly timing out).
LimitNOFILE=131072
# Limit processes count to something reasonable to
# prevent resources exhausting due to big amounts of helper
# processes launched.
LimitNPROC=512
# Restart server on any problem.
Restart=on-failure
# ... Unless it is a configuration problem.
RestartPreventExitStatus=2
ExecStart=/usr/local/bin/maddy run
ExecReload=/bin/kill -USR1 $MAINPID
ExecReload=/bin/kill -USR2 $MAINPID
[Install]
WantedBy=multi-user.target

78
dist/systemd/maddy@.service vendored Normal file
View File

@ -0,0 +1,78 @@
[Unit]
Description=maddy mail server (using %i.conf)
Documentation=man:maddy(1)
Documentation=man:maddy.conf(5)
Documentation=https://maddy.email
After=network-online.target
[Service]
Type=notify
NotifyAccess=main
User=maddy
Group=maddy
ConfigurationDirectory=maddy
RuntimeDirectory=maddy
StateDirectory=maddy
LogsDirectory=maddy
ReadOnlyPaths=/usr/lib/maddy
ReadWritePaths=/var/lib/maddy
# Strict sandboxing. You have no reason to trust code written by strangers from GitHub.
PrivateTmp=true
PrivateHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectHostname=true
ProtectClock=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
DeviceAllow=/dev/syslog
# Additional sandboxing. You need to disable all of these options
# for privileged helper binaries (for system auth) to work correctly.
NoNewPrivileges=true
PrivateDevices=true
RestrictSUIDSGID=true
ProtectKernelModules=true
MemoryDenyWriteExecute=true
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true
# Graceful shutdown with a reasonable timeout.
TimeoutStopSec=7s
KillMode=mixed
KillSignal=SIGTERM
# Required to bind on ports lower than 1024.
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# Force all files created by maddy to be only readable by it and
# maddy group.
UMask=0007
# Bump FD limitations. Even idle mail server can have a lot of FDs open (think
# of idle IMAP connections, especially ones abandoned on the other end and
# slowly timing out).
LimitNOFILE=131072
# Limit processes count to something reasonable to
# prevent resources exhausting due to big amounts of helper
# processes launched.
LimitNPROC=512
# Restart server on any problem.
Restart=on-failure
# ... Unless it is a configuration problem.
RestartPreventExitStatus=2
ExecStart=/usr/local/bin/maddy --config /etc/maddy/%i.conf run
ExecReload=/bin/kill -USR1 $MAINPID
ExecReload=/bin/kill -USR2 $MAINPID
[Install]
WantedBy=multi-user.target

1
dist/vim/ftdetect/maddy-conf.vim vendored Normal file
View File

@ -0,0 +1 @@
au BufNewFile,BufRead /etc/maddy/*,maddy.conf setf maddy-conf

8
dist/vim/ftplugin/maddy-conf.vim vendored Normal file
View File

@ -0,0 +1,8 @@
setlocal commentstring=#\ %s
" That is convention for maddy configs. Period.
" - fox.cpp (maddy developer)
setlocal expandtab
setlocal tabstop=4
setlocal softtabstop=4
setlocal shiftwidth=4

225
dist/vim/syntax/maddy-conf.vim vendored Normal file
View File

@ -0,0 +1,225 @@
" vim: noexpandtab ts=4 sw=4
if exists("b:current_syntax")
finish
endif
" Lexer-defined rules
syn match maddyComment "#.*"
syn region maddyString start=+"+ skip=+\\\\\|\\"+ end=+"+ oneline
syn region maddyBlock start="{" end="}" transparent fold
hi def link maddyComment Comment
hi def link maddyString String
" Parser-defined rules
syn match maddyMacroName "[a-z0-9_]" contained containedin=maddyMacro
syn match maddyMacro "$(.\{-})" contains=maddyMacroName
syn match maddyMacroDefSign "=" contained
syn match maddyMacroDef "\^$([a-z0-9_]\{-})\s=\s.\+" contains=maddyMacro,maddyMacroDefSign
hi def link maddyMacroName Identifier
hi def link maddyMacro Special
hi def link maddyMacroDefSign Special
" config.Map values
syn keyword maddyBool yes no
syn match maddyInt '\<\d\+\>'
syn match maddyInt '\<[-+]\d\+\>'
syn match maddyFloat '\<[-+]\d\+\.\d*\<'
syn match maddyReference /[ \t]&[^ \t]\+/ms=s+1 contains=maddyReferenceSign
syn match maddyReferenceSign /&/ contained
hi def link maddyBool Boolean
hi def link maddyInt Number
hi def link maddyFloat Float
hi def link maddyReferenceSign Special
" Module values
" grep --no-file -E 'Register.*\(".+", ' **.go | sed -E 's/.+Register.*\("([^"]+)", .+/\1/' | sort -u
syn keyword maddyModule
\ checks
\ command
\ dane
\ dkim
\ dnsbl
\ dnssec
\ dummy
\ extauth
\ external
\ file
\ identity
\ imap
\ imap_filters
\ imapsql
\ limits
\ lmtp
\ loader
\ local_policy
\ milter
\ modifiers
\ msgpipeline
\ mtasts
\ mx_auth
\ pam
\ pass_table
\ plain_separate
\ queue
\ regexp
\ remote
\ replace_rcpt
\ replace_sender
\ require_matching_rdns
\ require_mx_record
\ require_tls
\ rspamd
\ shadow
\ smtp
\ sql_query
\ sql_table
\ static
\ submission
syn keyword maddyDispatchDir
\ check
\ modify
\ default_source
\ source
\ default_destination
\ destination
\ reject
\ deliver_to
\ reroute
\ dmarc
" grep --no-file -E 'cfg..+\(".+", ' **.go | sed -E 's/.+cfg..+\("([^"]+)", .+/\1/' | sort -u
syn keyword maddyModDir
\ add
\ add_header_action
\ allow_multiple_from
\ api_path
\ appendlimit
\ attempt_starttls
\ auth
\ autogenerated_msg_domain
\ body_canon
\ bounce
\ broken_sig_action
\ buffer
\ cache
\ case_insensitive
\ certs
\ check_early
\ client_ipv4
\ client_ipv6
\ compression
\ conn_max_idle_count
\ conn_max_idle_time
\ conn_reuse_limit
\ debug
\ defer_sender_reject
\ del
\ domains
\ driver
\ dsn
\ ehlo
\ endpoint
\ enforce_early
\ enforce_testing
\ entry
\ error_resp_action
\ expand_replaceholders
\ fail_action
\ fail_open
\ file
\ flags
\ force_ipv4
\ fs_dir
\ fsstore
\ full_match
\ hash
\ header_canon
\ helper
\ hostname
\ imap_filter
\ init
\ insecure_auth
\ io_debug
\ io_error_action
\ io_errors
\ junk_mailbox
\ key_column
\ key_path
\ keys
\ limits
\ list
\ local_ip
\ location
\ lookup
\ mailfrom
\ max_logged_rcpt_errors
\ max_message_size
\ max_parallelism
\ max_received
\ max_recipients
\ max_tries
\ min_mx_level
\ min_tls_level
\ mx_auth
\ neutral_action
\ newkey_algo
\ none_action
\ no_sig_action
\ oversign_fields
\ pass
\ perdomain
\ permerr_action
\ quarantine_threshold
\ read_timeout
\ reject_threshold
\ relaxed_requiretls
\ required_fields
\ require_sender_match
\ require_tls
\ requiretls_override
\ responses
\ rewrite_subj_action
\ run_on
\ score
\ selector
\ set
\ settings_id
\ sig_expiry
\ sign_fields
\ sign_subdomains
\ softfail_action
\ SOME_action
\ source
\ sqlite3_busy_timeout
\ sqlite3_cache_size
\ sqlite3_exclusive_lock
\ storage
\ table
\ table_name
\ tag
\ target
\ targets
\ temperr_action
\ tls
\ tls_client
\ use_helper
\ user
\ value_column
\ write_timeout
hi def link maddyModDir Identifier
hi def link maddyModule Identifier
hi def link maddyDispatchDir Identifier
let b:current_syntax = "maddy"

81
docs/docker.md Normal file
View File

@ -0,0 +1,81 @@
# 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.7 creds create foxcpp@maddy.test
docker run --rm -it -v maddydata:/data foxcpp/maddy:0.7 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
```
## Build Tags
Some Maddy features (such as automatic certificate management via ACME with [a non-default libdns provider](../reference/tls-acme/#dns-providers)) require build tags to be passed to Maddy's `build.sh`, as this is run in the Dockerfile you must compile your own Docker image. Build tags can be set via the docker build argument `ADDITIONAL_BUILD_TAGS` e.g. `docker build --build-arg ADDITIONAL_BUILD_TAGS="libdns_acmedns libdns_route53" -t yourorgname/maddy:yourtagname .`.
## TL;DR
```
docker volume create maddydata
docker run \
--name maddy \
-e MADDY_HOSTNAME=mx.maddy.test \
-e MADDY_DOMAIN=maddy.test \
-v maddydata:/data \
-p 25:25 \
-p 143:143 \
-p 465:465 \
-p 587:587 \
-p 993:993 \
foxcpp/maddy:0.7
```
It will fail on first startup. Copy TLS certificate to /data/tls/fullchain.pem
and key to /data/tls/privkey.pem. Run the server again. Finish DNS configuration
(DKIM keys, etc) as described in [tutorials/setting-up/](../tutorials/setting-up/).

119
docs/faq.md Normal file
View File

@ -0,0 +1,119 @@
# 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](https://git.sr.ht/~migadu/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.

22
docs/index.md Normal file
View File

@ -0,0 +1,22 @@
> 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](https://builds.sr.ht/~emersion/maddy.svg)](https://builds.sr.ht/~emersion/maddy?)
[![License text](https://img.shields.io/github/license/foxcpp/maddy)](https://github.com/foxcpp/maddy/blob/master/LICENSE)
[![Issues tracker](https://img.shields.io/github/issues/foxcpp/maddy)](https://github.com/foxcpp/maddy)
* [Setup tutorial](https://maddy.email/tutorials/setting-up/)
* [IRC channel](https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1)
* [Mailing list](https://lists.sr.ht/~foxcpp/maddy)

23
docs/internals/quirks.md Normal file
View File

@ -0,0 +1,23 @@
# 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.
[RFC 2821]: https://tools.ietf.org/html/rfc2821
[RFC 3501]: https://tools.ietf.org/html/rfc3501

View File

@ -0,0 +1,291 @@
# 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](unicode.md) page for
details.
## Misc
- [RFC 5782] - DNS Blacklists and Whitelists
[GH 188]: https://github.com/foxcpp/maddy/issues/188
[RFC 2822]: https://tools.ietf.org/html/rfc2822
[RFC 2045]: https://tools.ietf.org/html/rfc2045
[RFC 2046]: https://tools.ietf.org/html/rfc2046
[RFC 2047]: https://tools.ietf.org/html/rfc2047
[RFC 2048]: https://tools.ietf.org/html/rfc2048
[RFC 2049]: https://tools.ietf.org/html/rfc2049
[RFC 6532]: https://tools.ietf.org/html/rfc6532
[RFC 2183]: https://tools.ietf.org/html/rfc2183
[RFC 3501]: https://tools.ietf.org/html/rfc3501
[RFC 2152]: https://tools.ietf.org/html/rfc2152
[RFC 2595]: https://tools.ietf.org/html/rfc2595
[RFC 7889]: https://tools.ietf.org/html/rfc7889
[RFC 3348]: https://tools.ietf.org/html/rfc3348
[RFC 6851]: https://tools.ietf.org/html/rfc6851
[RFC 6154]: https://tools.ietf.org/html/rfc6154
[RFC 5255]: https://tools.ietf.org/html/rfc5255
[RFC 4978]: https://tools.ietf.org/html/rfc4978
[RFC 3691]: https://tools.ietf.org/html/rfc3691
[RFC 2177]: https://tools.ietf.org/html/rfc2177
[RFC 7888]: https://tools.ietf.org/html/rfc7888
[RFC 4959]: https://tools.ietf.org/html/rfc4959
[RFC 2033]: https://tools.ietf.org/html/rfc2033
[RFC 5321]: https://tools.ietf.org/html/rfc5321
[RFC 6409]: https://tools.ietf.org/html/rfc6409
[RFC 1870]: https://tools.ietf.org/html/rfc1870
[RFC 2920]: https://tools.ietf.org/html/rfc2920
[RFC 2034]: https://tools.ietf.org/html/rfc2034
[RFC 3207]: https://tools.ietf.org/html/rfc3207
[RFC 4954]: https://tools.ietf.org/html/rfc4954
[RFC 6152]: https://tools.ietf.org/html/rfc6152
[RFC 6531]: https://tools.ietf.org/html/rfc6531
[RFC 6522]: https://tools.ietf.org/html/rfc6522
[RFC 3464]: https://tools.ietf.org/html/rfc3464
[RFC 6533]: https://tools.ietf.org/html/rfc6533
[RFC 4422]: https://tools.ietf.org/html/rfc4422
[RFC 4616]: https://tools.ietf.org/html/rfc4616
[RFC 6376]: https://tools.ietf.org/html/rfc6376
[RFC 7001]: https://tools.ietf.org/html/rfc7001
[RFC 7208]: https://tools.ietf.org/html/rfc7208
[RFC 7372]: https://tools.ietf.org/html/rfc7372
[RFC 7479]: https://tools.ietf.org/html/rfc7479
[RFC 8301]: https://tools.ietf.org/html/rfc8301
[RFC 8463]: https://tools.ietf.org/html/rfc8463
[RFC 8616]: https://tools.ietf.org/html/rfc8616
[RFC 4033]: https://tools.ietf.org/html/rfc4033
[RFC 6698]: https://tools.ietf.org/html/rfc6698
[RFC 7672]: https://tools.ietf.org/html/rfc7672
[RFC 8461]: https://tools.ietf.org/html/rfc8461
[RFC 3492]: https://tools.ietf.org/html/rfc3492
[RFC 3629]: https://tools.ietf.org/html/rfc3629
[RFC 5890]: https://tools.ietf.org/html/rfc5890
[RFC 5891]: https://tools.ietf.org/html/rfc5891
[RFC 7616]: https://tools.ietf.org/html/rfc7616
[RFC 8264]: https://tools.ietf.org/html/rfc8264
[RFC 5782]: https://tools.ietf.org/html/rfc5782
[RFC 2822]: https://tools.ietf.org/html/rfc2822
[RFC 2045]: https://tools.ietf.org/html/rfc2045
[RFC 2046]: https://tools.ietf.org/html/rfc2046
[RFC 2047]: https://tools.ietf.org/html/rfc2047
[RFC 2048]: https://tools.ietf.org/html/rfc2048
[RFC 2049]: https://tools.ietf.org/html/rfc2049
[RFC 6532]: https://tools.ietf.org/html/rfc6532
[RFC 3501]: https://tools.ietf.org/html/rfc3501
[RFC 2595]: https://tools.ietf.org/html/rfc2595
[RFC 7889]: https://tools.ietf.org/html/rfc7889
[RFC 3348]: https://tools.ietf.org/html/rfc3348
[RFC 6851]: https://tools.ietf.org/html/rfc6851
[RFC 6154]: https://tools.ietf.org/html/rfc6154
[RFC 5255]: https://tools.ietf.org/html/rfc5255
[RFC 4978]: https://tools.ietf.org/html/rfc4978
[RFC 3691]: https://tools.ietf.org/html/rfc3691
[RFC 2177]: https://tools.ietf.org/html/rfc2177
[RFC 7888]: https://tools.ietf.org/html/rfc7888
[RFC 4959]: https://tools.ietf.org/html/rfc4959
[RFC 2033]: https://tools.ietf.org/html/rfc2033
[RFC 5321]: https://tools.ietf.org/html/rfc5321
[RFC 6409]: https://tools.ietf.org/html/rfc6409
[RFC 1870]: https://tools.ietf.org/html/rfc1870
[RFC 2920]: https://tools.ietf.org/html/rfc2920
[RFC 2034]: https://tools.ietf.org/html/rfc2034
[RFC 3207]: https://tools.ietf.org/html/rfc3207
[RFC 4954]: https://tools.ietf.org/html/rfc4954
[RFC 6152]: https://tools.ietf.org/html/rfc6152
[RFC 6531]: https://tools.ietf.org/html/rfc6531
[RFC 6522]: https://tools.ietf.org/html/rfc6522
[RFC 3464]: https://tools.ietf.org/html/rfc3464
[RFC 6533]: https://tools.ietf.org/html/rfc6533
[RFC 4422]: https://tools.ietf.org/html/rfc4422
[RFC 4616]: https://tools.ietf.org/html/rfc4616
[RFC 6376]: https://tools.ietf.org/html/rfc6376
[RFC 7001]: https://tools.ietf.org/html/rfc7001
[RFC 7208]: https://tools.ietf.org/html/rfc7208
[RFC 7372]: https://tools.ietf.org/html/rfc7372
[RFC 7479]: https://tools.ietf.org/html/rfc7479
[RFC 8301]: https://tools.ietf.org/html/rfc8301
[RFC 8463]: https://tools.ietf.org/html/rfc8463
[RFC 8616]: https://tools.ietf.org/html/rfc8616
[RFC 4033]: https://tools.ietf.org/html/rfc4033
[RFC 6698]: https://tools.ietf.org/html/rfc6698
[RFC 7672]: https://tools.ietf.org/html/rfc7672
[RFC 8461]: https://tools.ietf.org/html/rfc8461
[RFC 3492]: https://tools.ietf.org/html/rfc3492
[RFC 3629]: https://tools.ietf.org/html/rfc3629
[RFC 5890]: https://tools.ietf.org/html/rfc5890
[RFC 5891]: https://tools.ietf.org/html/rfc5891
[RFC 7616]: https://tools.ietf.org/html/rfc7616
[RFC 8264]: https://tools.ietf.org/html/rfc8264
[RFC 5782]: https://tools.ietf.org/html/rfc5782
[RFC 2822]: https://tools.ietf.org/html/rfc2822
[RFC 2045]: https://tools.ietf.org/html/rfc2045
[RFC 2046]: https://tools.ietf.org/html/rfc2046
[RFC 2047]: https://tools.ietf.org/html/rfc2047
[RFC 2048]: https://tools.ietf.org/html/rfc2048
[RFC 2049]: https://tools.ietf.org/html/rfc2049
[RFC 6532]: https://tools.ietf.org/html/rfc6532
[RFC 3501]: https://tools.ietf.org/html/rfc3501
[RFC 2595]: https://tools.ietf.org/html/rfc2595
[RFC 7889]: https://tools.ietf.org/html/rfc7889
[RFC 3348]: https://tools.ietf.org/html/rfc3348
[RFC 6851]: https://tools.ietf.org/html/rfc6851
[RFC 6154]: https://tools.ietf.org/html/rfc6154
[RFC 5255]: https://tools.ietf.org/html/rfc5255
[RFC 4978]: https://tools.ietf.org/html/rfc4978
[RFC 3691]: https://tools.ietf.org/html/rfc3691
[RFC 2177]: https://tools.ietf.org/html/rfc2177
[RFC 7888]: https://tools.ietf.org/html/rfc7888
[RFC 4959]: https://tools.ietf.org/html/rfc4959
[RFC 2033]: https://tools.ietf.org/html/rfc2033
[RFC 5321]: https://tools.ietf.org/html/rfc5321
[RFC 6409]: https://tools.ietf.org/html/rfc6409
[RFC 1870]: https://tools.ietf.org/html/rfc1870
[RFC 2920]: https://tools.ietf.org/html/rfc2920
[RFC 2034]: https://tools.ietf.org/html/rfc2034
[RFC 3207]: https://tools.ietf.org/html/rfc3207
[RFC 4954]: https://tools.ietf.org/html/rfc4954
[RFC 6152]: https://tools.ietf.org/html/rfc6152
[RFC 6531]: https://tools.ietf.org/html/rfc6531
[RFC 6522]: https://tools.ietf.org/html/rfc6522
[RFC 3464]: https://tools.ietf.org/html/rfc3464
[RFC 6533]: https://tools.ietf.org/html/rfc6533
[RFC 4422]: https://tools.ietf.org/html/rfc4422
[RFC 4616]: https://tools.ietf.org/html/rfc4616
[RFC 6376]: https://tools.ietf.org/html/rfc6376
[RFC 8301]: https://tools.ietf.org/html/rfc8301
[RFC 8463]: https://tools.ietf.org/html/rfc8463
[RFC 7208]: https://tools.ietf.org/html/rfc7208
[RFC 7372]: https://tools.ietf.org/html/rfc7372
[RFC 7479]: https://tools.ietf.org/html/rfc7479
[RFC 8616]: https://tools.ietf.org/html/rfc8616
[RFC 4033]: https://tools.ietf.org/html/rfc4033
[RFC 6698]: https://tools.ietf.org/html/rfc6698
[RFC 7672]: https://tools.ietf.org/html/rfc7672
[RFC 8461]: https://tools.ietf.org/html/rfc8461
[RFC 3492]: https://tools.ietf.org/html/rfc3492
[RFC 3629]: https://tools.ietf.org/html/rfc3629
[RFC 5890]: https://tools.ietf.org/html/rfc5890
[RFC 5891]: https://tools.ietf.org/html/rfc5891
[RFC 7616]: https://tools.ietf.org/html/rfc7616
[RFC 8264]: https://tools.ietf.org/html/rfc8264
[RFC 5782]: https://tools.ietf.org/html/rfc5782
[Unicode 11.0.0]: https://www.unicode.org/versions/components-11.0.0.html
[UAX #15]: https://unicode.org/reports/tr15/

49
docs/internals/sqlite.md Normal file
View File

@ -0,0 +1,49 @@
# 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.

96
docs/internals/unicode.md Normal file
View File

@ -0,0 +1,96 @@
# Unicode support
maddy has the first-class Unicode support in all components (modules). You do
not have to take any actions to make it work with internationalized domains,
mailbox names or non-ASCII message headers.
Internally, all text fields in maddy are represented in UTF-8 and handled using
Unicode-aware operations for comparisons, case-folding and so on.
## Non-ASCII data in message headers and bodies
maddy SMTP implementation does not care about encodings used in MIME headers or
in `Content-Type text/*` charset field.
However, local IMAP storage implementation needs to perform certain operations
on header contents. This is mostly about SEARCH functionality. For IMAP search
to work correctly, the message body and headers should use one of the following
encodings:
- ASCII
- UTF-8
- ISO-8859-1, 2, 3, 4, 9, 10, 13, 14, 15 or 16
- Windows-1250, 1251 or 1252 (aka Code Page 1250 and so on)
- KOI8-R
- ~~HZGB2312~~, GB18030
- GBK (aka Code Page 936)
- Shift JIS (aka Code Page 932 or Windows-31J)
- Big-5 (aka Code Page 950)
- EUC-JP
- ISO-2022-JP
_Support for HZGB2312 is currently disabled due to bugs with security
implications._
If mailbox includes a message with any encoding not listed here, it will not
be returned in search results for any request.
Behavior regarding handling of non-Unicode encodings is not considered stable
and may change between versions (including removal of supported encodings). If
you need your stuff to work correctly - start using UTF-8.
## Configuration files
maddy configuration files are assumed to be encoded in UTF-8. Use of any other
encoding will break stuff, do not do it.
Domain names (e.g. in hostname directive or pipeline rules) can be represented
using the ACE form (aka Punycode). They will be converted to the Unicode form
internally.
## Local credentials
'sql' storage backend and authentication provider enforce a number of additional
constraints on used account names.
PRECIS UsernameCaseMapped profile is enforced for local email addresses.
It limits the use of control and Bidi characters to make sure the used value
can be represented consistently in a variety of contexts. On top of that, the
address is case-folded and normalized to the NFC form for consistent internal
handling.
PRECIS OpaqueString profile is enforced for passwords. Less strict rules are
applied here. Runs of Unicode whitespace characters are replaced with a single
ASCII space. NFC normalization is applied afterwards. If the resulting string
is empty - the password is not accepted.
Both profiles are defined in RFC 8265, consult it for details.
## Protocol support
### SMTPUTF8 extension
maddy SMTP implementation includes support for the SMTPUTF8 extension as
defined in RFC 6531.
This means maddy can handle internationalized mailbox and domain names in MAIL
FROM, RCPT TO commands both for outbound and inbound delivery.
maddy will not accept messages with non-ASCII envelope addresses unless
SMTPUTF8 support is requested. If a message with SMTPUTF8 flag set is forwarded
to a server without SMTPUTF8 support, delivery will fail unless it is possible
to represent envelope addresses in the ASCII form (only domains use Unicode and
they can be converted to Punycode). Contents of message body (and header) are
not considered and always accepted and sent as-is, no automatic downgrading or
reencoding is done.
### IMAP UTF8, I18NLEVEL extensions
Currently, maddy does not include support for UTF8 and I18NLEVEL IMAP
extensions. However, it is not a problem that can prevent it from correctly
handling UTF-8 messages (or even messages in other non-ASCII encodings
mentioned above).
Clients that want to implement proper handling for Unicode strings may assume
maddy does not handle them properly in e.g. SEARCH commands and so such clients
may download messages and process them locally.

1
docs/man/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_generated_*.md

16
docs/man/README.md Normal file
View File

@ -0,0 +1,16 @@
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.

41
docs/man/maddy.1.scd Normal file
View File

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

57
docs/man/prepare_md.py Normal file
View File

@ -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'\+\+$', '<br>', 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'))

157
docs/multiple-domains.md Normal file
View File

@ -0,0 +1,157 @@
# Multiple domains configuration
By default, maddy uses email addresses as account identifiers for both
authentication and storage purposes. Therefore, account named `user@example.org`
is completely independent from `user@example.com`. They must be created
separately, may have different credentials and have separate IMAP mailboxes.
This makes it extremely easy to setup maddy to manage multiple otherwise
independent domains.
Default configuration file contains two macros - `$(primary_domain)` and
`$(local_domains)`. They are used to used in several places thorough the
file to configure message routing, security checks, etc.
In general, you should just add all domains you want maddy to manage to
`$(local_domains)`, like this:
```
$(primary_domain) = example.org
$(local_domains) = $(primary_domain) example.com
```
Note that you need to pick one domain as a "primary" for use in
auto-generated messages.
With that done, you can create accounts using both domains in the name, send
and receive messages and so on. Do not forget to configure corresponding SPF,
DMARC and MTA-STS records as was recommended in
the [introduction tutorial](tutorials/setting-up.md).
Also note that you do not really need a separate TLS certificate for each
managed domain. You can have one hostname e.g. mail.example.org set as an MX
record for multiple domains.
**If you want multiple domains to share username namespace**, you should change
several more options.
You can make "user@example.org" and "user@example.com" users share the same
credentials of user "user" but have different IMAP mailboxes ("user@example.org"
and "user@example.com" correspondingly). For that, it is enough to set `auth_map`
globally to use `email_localpart` table:
```
auth_map email_localpart
```
This way, when user logs in as "user@example.org", "user" will be passed
to the authentication provider, but "user@example.org" will be passed to the
storage backend. You should create accounts like this:
```
maddy creds create user
maddy imap-acct create user@example.org
maddy imap-acct create user@example.com
```
**If you want accounts to also share the same IMAP storage of account named
"user"**, you can set `storage_map` in IMAP endpoint and `delivery_map` in
storage backend to use `email_locapart`:
```
storage.imapsql local_mailboxes {
...
delivery_map email_localpart # deliver "user@*" to "user"
}
imap tls://0.0.0.0:993 {
...
storage &local_mailboxes
...
storage_map email_localpart # "user@*" accesses "user" mailbox
}
```
You also might want to make it possible to log in without
specifying a domain at all. In this case, use `email_localpart_optional` for
both `auth_map` and `storage_map`.
You also need to make `authorize_sender` check (used in `submission` endpoint)
accept non-email usernames:
```
authorize_sender {
...
user_to_email chain {
step email_localpart_optional # remove domain from username if present
step email_with_domain $(local_domains) # expand username with all allowed domains
}
}
```
## TL;DR
Your options:
**"user@example.org" and "user@example.com" have distinct credentials and
distinct mailboxes.**
```
$(primary_domain) = example.org
$(local_domains) = example.org example.com
```
Create accounts as:
```shell
maddy creds create user@example.org
maddy imap-acct create user@example.org
maddy creds create user@example.com
maddy imap-acct create user@example.com
```
**"user@example.org" and "user@example.com" have same credentials but
distinct mailboxes.**
```
$(primary_domain) = example.org
$(local_domains) = example.org example.com
auth_map email_localpart
```
Create accounts as:
```shell
maddy creds create user
maddy imap-acct create user@example.org
maddy imap-acct create user@example.com
```
**"user@example.org", "user@example.com", "user" have same credentials and same
mailboxes.**
```
$(primary_domain) = example.org
$(local_domains) = example.org example.com
auth_map email_localpart_optional # authenticating as "user@*" checks credentials for "user"
storage.imapsql local_mailboxes {
...
delivery_map email_localpart_optional # deliver "user@*" to "user" mailbox
}
imap tls://0.0.0.0:993 {
...
storage_map email_localpart_optional # authenticating as "user@*" accesses "user" mailboxes
}
submission tls://0.0.0.0:465 {
check {
authorize_sender {
...
user_to_email chain {
step email_localpart_optional # remove domain from username if present
step email_with_domain $(local_domains) # expand username with all allowed domains
}
}
}
...
}
```
Create accounts as:
```shell
maddy creds create user
maddy imap-acct create user
```

View File

@ -0,0 +1,26 @@
# 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
### 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.

View File

@ -0,0 +1,52 @@
# System command
auth.external module for authentication using external helper binary. It looks for binary
named `maddy-auth-helper` in $PATH and libexecdir and uses it for authentication
using username/password pair.
The protocol is very simple:
Program is launched for each authentication. Username and password are written
to stdin, adding \n to the end. If binary exits with 0 status code -
authentication is considered successful. If the status code is 1 -
authentication is failed. If the status code is 2 - another unrelated error has
happened. Additional information should be written to stderr.
```
auth.external {
helper /usr/bin/ldap-helper
perdomain no
domains example.org
}
```
## Configuration directives
### helper _file_path_
**Required.** <br>
Location of the helper binary.
---
### perdomain _boolean_
Default: `no`
Don't remove domain part of username when authenticating and require it to be
present. Can be used if you want user@domain1 and user@domain2 to be different
accounts.
---
### domains _domains..._
Default: not specified
Domains that should be allowed in username during authentication.
For example, if 'domains' is set to "domain1 domain2", then
username, username@domain1 and username@domain2 will be accepted as valid login
name in addition to just username.
If used without 'perdomain', domain part will be removed from login before
check with underlying auth. mechanism. If 'perdomain' is set, then
domains must be also set and domain part **will not** be removed before check.

130
docs/reference/auth/ldap.md Normal file
View File

@ -0,0 +1,130 @@
# LDAP BindDN
maddy supports authentication via LDAP using DN binding. Passwords are verified
by the LDAP server.
maddy needs to know the DN to use for binding. It can be obtained either by
directory search or template .
Note that storage backends conventionally use email addresses, if you use
non-email identifiers as usernames then you should map them onto
emails on delivery by using `auth_map` (see documentation page for used storage backend).
auth.ldap also can be a used as a table module. This way you can check
whether the account exists. It works only if DN template is not used.
```
auth.ldap {
urls ldap://maddy.test:389
# Specify initial bind credentials. Not required ('bind off')
# if DN template is used.
bind plain "cn=maddy,ou=people,dc=maddy,dc=test" "123456"
# Specify DN template to skip lookup.
dn_template "cn={username},ou=people,dc=maddy,dc=test"
# Specify base_dn and filter to lookup DN.
base_dn "ou=people,dc=maddy,dc=test"
filter "(&(objectClass=posixAccount)(uid={username}))"
tls_client { ... }
starttls off
debug off
connect_timeout 1m
}
```
```
auth.ldap ldap://maddy.test.389 {
...
}
```
## Configuration directives
### urls _servers..._
**Required.**
URLs of the directory servers to use. First available server
is used - no load-balancing is done.
URLs should use `ldap://`, `ldaps://`, `ldapi://` schemes.
---
### bind `off` | `unauth` | `external` | `plain` _username_ _password_
Default: `off`
Credentials to use for initial binding. Required if DN lookup is used.
`unauth` performs unauthenticated bind. `external` performs external binding
which is useful for Unix socket connections (`ldapi://`) or TLS client certificate
authentication (cert. is set using tls_client directive). `plain` performs a
simple bind using provided credentials.
---
### dn_template _template_
DN template to use for binding. `{username}` is replaced with the
username specified by the user.
---
### base_dn _dn_
Base DN to use for lookup.
---
### filter _str_
DN lookup filter. `{username}` is replaced with the username specified
by the user.
Example:
```
(&(objectClass=posixAccount)(uid={username}))
```
Example (using ActiveDirectory):
```
(&(objectCategory=Person)(memberOf=CN=user-group,OU=example,DC=example,DC=org)(sAMAccountName={username})(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))
```
Example:
```
(&(objectClass=Person)(mail={username}))
```
---
### starttls _bool_
Default: `off`
Whether to upgrade connection to TLS using STARTTLS.
---
### tls_client { ... }
Advanced TLS client configuration. See [TLS configuration / Client](/reference/tls/#client) for details.
---
### connect_timeout _duration_
Default: `1m`
Timeout for initial connection to the directory server.
---
### request_timeout _duration_
Default: `1m`
Timeout for each request (binding, lookup).

View File

@ -0,0 +1,48 @@
# Native NetAuth
maddy supports authentication via NetAuth using direct entity
authentication checks. Passwords are verified by the NetAuth server.
maddy needs to know the Entity ID to use for authentication. It must
match the string the user provides for the Local Atom part of their
mail address.
Note that storage backends conventionally use email addresses. Since
NetAuth recommends *nix compatible usernames, you will need to map the
email identifiers to NetAuth Entity IDs using `auth_map` (see
documentation page for used storage backend).
auth.netauth also can be used as a table module. This way you can
check whether the account exists.
Note that the configuration fragment provided below is very sparse.
This is because NetAuth expects to read most of its common
configuration values from the system NetAuth config file located at
`/etc/netauth/config.toml`.
```
auth.netauth {
require_group "maddy-users"
debug off
}
```
```
auth.netauth {}
```
## Configuration directives
### require_group _group_
Optional.
Group that entities must possess to be able to use maddy services.
This can be used to provide email to just a subset of the entities
present in NetAuth.
---
### debug `on` | `off`
Default: `off`

View File

@ -0,0 +1,48 @@
# PAM
auth.pam module implements authentication using libpam. Alternatively it can be configured to
use helper binary like auth.external module does.
maddy should be built with libpam build tag to use this module without
'use_helper' directive.
```
go get -tags 'libpam' ...
```
```
auth.pam {
debug no
use_helper no
}
```
## Configuration directives
### debug _boolean_
Default: `no`
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.
---
### use_helper _boolean_
Default: `no`
Use `LibexecDirectory/maddy-pam-helper` instead of directly calling libpam.
You need to use that if:
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
```

View File

@ -0,0 +1,44 @@
# 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.

View File

@ -0,0 +1,45 @@
# Separate username and password lookup
auth.plain_separate module implements authentication using username:password pairs but can
use zero or more "table modules" (maddy-tables(5)) and one or more
authentication providers to verify credentials.
```
auth.plain_separate {
user ...
user ...
...
pass ...
pass ...
...
}
```
How it works:
- Initial username input is normalized using PRECIS UsernameCaseMapped profile.
- Each table specified with the 'user' directive looked up using normalized
username. If match is not found in any table, authentication fails.
- Each authentication provider specified with the 'pass' directive is tried.
If authentication with all providers fails - an error is returned.
## Configuration directives
### user _table-module_
Configuration block for any module from maddy-tables(5) can be used here.
Example:
```
user file /etc/maddy/allowed_users
```
---
### 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.

View File

@ -0,0 +1,40 @@
# /etc/shadow
auth.shadow module implements authentication by reading /etc/shadow. Alternatively it can be
configured to use helper binary like auth.external does.
```
auth.shadow {
debug no
use_helper no
}
```
## Configuration directives
### debug _boolean_
Default: `no`
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.
---
### use_helper _boolean_
Default: `no`
Use `LibexecDirectory/maddy-shadow-helper` instead of directly reading `/etc/shadow`.
You need to use that if maddy is running as an unprivileged user
privileges (e.g. when using system accounts).
You need to make `maddy-shadow-helper` binary setuid, see
cmd/maddy-shadow-helper/README.md in source tree for details.
TL;DR (assuming you have maddy group):
```
chown root:maddy /usr/lib/maddy/maddy-shadow-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-shadow-helper
```

23
docs/reference/blob/fs.md Normal file
View File

@ -0,0 +1,23 @@
# Filesystem
This module stores message bodies in a file system directory.
```
storage.blob.fs {
root <directory>
}
```
```
storage.blob.fs <directory>
```
## Configuration directives
### root _path_
Default: not set
Path to the FS directory. Must be readable and writable by the server process.
If it does not exist - it will be created (parent directory should be writable
for this). Relative paths are interpreted relatively to server state directory.

98
docs/reference/blob/s3.md Normal file
View File

@ -0,0 +1,98 @@
# Amazon S3
storage.blob.s3 module stores messages bodies in a bucket on S3-compatible storage.
```
storage.blob.s3 {
endpoint play.min.io
secure yes
access_key "Q3AM3UQ867SPQQA43P2F"
secret_key "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
bucket maddy-test
# optional
region eu-central-1
object_prefix maddy/
creds access_key
}
```
Example:
```
storage.imapsql local_mailboxes {
...
msg_store s3 {
endpoint s3.amazonaws.com
access_key "..."
secret_key "..."
bucket maddy-messages
region us-west-2
creds access_key
}
}
```
## Configuration directives
### endpoint _address:port_
**Required**.
Root S3 endpoint. e.g. `s3.amazonaws.com`
---
### secure _boolean_
Default: `yes`
Whether TLS should be used.
---
### access_key _string_<br>secret_key _string_
**Required**.
Static S3 credentials.
---
### bucket _name_
**Required**.
S3 bucket name. The bucket must exist and
be read-writable.
---
### region _string_
Default: not set
S3 bucket location. May be called "endpoint" in some manuals.
---
### object_prefix _string_
Default: empty string
String to add to all keys stored by maddy.
Can be useful when S3 is used as a file system.
---
### creds `access_key` | `file_minio` | `file_aws` | `iam`
Default: `access_key`
Credentials to use for accessing the S3 Bucket.
Credential Types:
- `access_key`: use AWS access key and secret access key
- `file_minio`: use credentials for Minio present at ~/.mc/config.json
- `file_aws`: use credentials for AWS S3 present at ~/.aws/credentials
- `iam`: use AWS IAM instance profile for credentials.
By default, access_key is used with the access key and secret access key present in the config.

View File

@ -0,0 +1,21 @@
# 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.

View File

@ -0,0 +1,132 @@
# MAIL FROM and From authorization
Module check.authorize_sender verifies that envelope and header sender addresses belong
to the authenticated user. Address ownership is established via table
that maps each user account to a email address it is allowed to use.
There are some special cases, see `user_to_email` description below.
```
check.authorize_sender {
prepare_email identity
user_to_email identity
check_header yes
unauth_action reject
no_match_action reject
malformed_action reject
err_action reject
auth_normalize auto
from_normalize auto
}
```
```
check {
authorize_sender { ... }
}
```
## Configuration directives
### user_to_email _table_
Default: `identity`
Table that maps authorization username to the list of sender emails
the user is allowed to use.
In additional to email addresses, the table can contain domain names or
special string "\*" as a value. If the value is a domain - user
will be allowed to use any mailbox within it as a sender address.
If it is "\*" - user will be allowed to use any address.
By default, table.identity is used, meaning that username should
be equal to the sender email.
Before username is looked up via the table, normalization algorithm
defined by auth_normalize is applied to it.
---
### prepare_email _table_
Default: `identity`
Table that is used to translate email addresses before they
are matched against user_to_email values.
Typically used to allow users to use their aliases as sender
addresses - prepare_email in this case should translate
aliases to "canonical" addresses. This is how it is
done in default configuration.
If table does not contain any mapping for the used sender
address, it will be used as is.
---
### check_header _boolean_
Default: `yes`
Whether to verify header sender in addition to envelope.
Either Sender or From field value should match the
authorization identity.
---
### unauth_action _action_
Default: `reject`
What to do if the user is not authenticated at all.
---
### no_match_action _action_
Default: `reject`
What to do if user is not allowed to use the sender address specified.
---
### malformed_action _action_
Default: `reject`
What to do if From or Sender header fields contain malformed values.
---
### err_action _action_
Default: `reject`
What to do if error happens during prepare_email or user_to_email lookup.
---
### auth_normalize _action_
Default: `auto`
Normalization function to apply to authorization username before
further processing.
Available options:
- `auto` `precis_casefold_email` for valid emails, `precis_casefold` otherwise.
- `precis_casefold_email` PRECIS UsernameCaseMapped profile + U-labels form for domain
- `precis_casefold` PRECIS UsernameCaseMapped profile for the entire string
- `precis_email` PRECIS UsernameCasePreserved profile + U-labels form for domain
- `precis` PRECIS UsernameCasePreserved profile for the entire string
- `casefold` Convert to lower case
- `noop` Nothing
PRECIS profiles are defined by RFC 8265. In short, they make sure
that Unicode strings that look the same will be compared as if they were
the same. CaseMapped profiles also convert strings to lower case.
---
### from_normalize _action_
Default: `auto`
Normalization function to apply to email addresses before
further processing.
Available options are same as for `auth_normalize`.

View File

@ -0,0 +1,96 @@
# System command filter
This module executes an arbitrary system command during a specified stage of
checks execution.
```
command executable_name arg0 arg1 ... {
run_on body
code 1 reject
code 2 quarantine
}
```
## Arguments
The module arguments specify the command to run. If the first argument is not
an absolute path, it is looked up in the Libexec Directory (/usr/lib/maddy on
Linux) and in $PATH (in that ordering). Note that no additional handling
of arguments is done, especially, the command is executed directly, not via the
system shell.
There is a set of special strings that are replaced with the corresponding
message-specific values:
- `{source_ip}` IPv4/IPv6 address of the sending MTA.
- `{source_host}` Hostname of the sending MTA, from the HELO/EHLO command.
- `{source_rdns}` PTR record of the sending MTA IP address.
- `{msg_id}` Internal message identifier. Unique for each delivery.
- `{auth_user}` Client username, if authenticated using SASL PLAIN
- `{sender}` Message sender address, as specified in the MAIL FROM SMTP command.
- `{rcpts}` List of accepted recipient addresses, including the currently handled
one.
- `{address}` Currently handled address. This is a recipient address if the command
is called during RCPT TO command handling (`run_on rcpt`) or a sender
address if the command is called during MAIL FROM command handling (`run_on
sender`).
If value is undefined (e.g. `{source_ip}` for a message accepted over a Unix
socket) or unavailable (the command is executed too early), the placeholder
is replaced with an empty string. Note that it can not remove the argument.
E.g. `-i {source_ip}` will not become just `-i`, it will be `-i ""`
Undefined placeholders are not replaced.
## Command stdout
The command stdout must be either empty or contain a valid RFC 5322 header.
If it contains a byte stream that does not look a valid header, the message
will be rejected with a temporary error.
The header from stdout will be **prepended** to the message header.
## Configuration directives
### run_on `conn` | `sender` | `rcpt` | `body`
Default: `body`
When to run the command. This directive also affects the information visible
for the message.
- `conn`<br>
Run before the sender address (MAIL FROM) is handled.<br>
**Stdin**: Empty <br>
**Available placeholders**: {source_ip}, {source_host}, {msg_id}, {auth_user}.
- `sender`<br>
Run during sender address (MAIL FROM) handling.<br>
**Stdin**: Empty <br>
**Available placeholders**: conn placeholders + {sender}, {address}.
The {address} placeholder contains the MAIL FROM address.
- `rcpt`<br>
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.<br>
**Stdin**: Empty <br>
**Available placeholders**: sender placeholders + {rcpts}.
The {address} placeholder contains the recipient address.
- `body`<br>
Run during message body handling.<br>
**Stdin**: The message header + body <br>
**Available placeholders**: all except for {address}.
---
### code _integer_ ignore <br>code _integer_ quarantine <br>code _integer_ reject _smtp-code_ _smtp-enhanced-code_ _smtp-message_
This directive specifies the mapping from the command exit code _integer_ to
the message pipeline action.
Two codes are defined implicitly, exit code 1 causes the message to be rejected
with a permanent error, exit code 2 causes the message to be quarantined. Both
actions can be overridden using the 'code' directive.

View File

@ -0,0 +1,63 @@
# DKIM
This is the check module that performs verification of the DKIM signatures
present on the incoming messages.
## Configuration directives
```
check.dkim {
debug no
required_fields From Subject
allow_body_subset no
no_sig_action ignore
broken_sig_action ignore
fail_open no
}
```
### debug _boolean_
Default: global directive value
Log both successful and unsuccessful check executions instead of just
unsuccessful.
---
### required_fields _string..._
Default: `From Subject`
Header fields that should be included in each signature. If signature
lacks any field listed in that directive, it will be considered invalid.
Note that From is always required to be signed, even if it is not included in
this directive.
---
### no_sig_action _action_
Default: `ignore` (recommended by RFC 6376)
Action to take when message without any signature is received.
Note that DMARC policy of the sender domain can request more strict handling of
missing DKIM signatures.
---
### broken_sig_action _action_
Default: `ignore` (recommended by RFC 6376)
Action to take when there are not valid signatures in a message.
Note that DMARC policy of the sender domain can request more strict handling of
broken DKIM signatures.
---
### fail_open _boolean_
Default: `no`
Whether to accept the message if a temporary error occurs during DKIM
verification. Rejecting the message with a 4xx code will require the sender
to resend it later in a hope that the problem will be resolved.

View File

@ -0,0 +1,173 @@
# DNSBL lookup
The check.dnsbl module implements checking of source IP and hostnames against a set
of DNS-based Blackhole lists (DNSBLs).
Its configuration consists of module configuration directives and a set
of blocks specifying lists to use and kind of lookups to perform on them.
```
check.dnsbl {
debug no
check_early no
quarantine_threshold 1
reject_threshold 1
# Lists configuration example.
dnsbl.example.org {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
score 1
}
hsrbl.example.org {
client_ipv4 no
client_ipv6 no
ehlo yes
mailfrom yes
score 1
}
}
```
## Arguments
Arguments specify the list of IP-based BLs to use.
The following configurations are equivalent.
```
check {
dnsbl dnsbl.example.org dnsbl2.example.org
}
```
```
check {
dnsbl {
dnsbl.example.org dnsbl2.example.org {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
score 1
}
}
}
```
## Configuration directives
### debug _boolean_
Default: global directive value
Enable verbose logging.
---
### check_early _boolean_
Default: `no`
Check BLs before mail delivery starts and silently reject blacklisted clients.
For this to work correctly, check should not be used in source/destination
pipeline block.
In particular, this means:
- No logging is done for rejected messages.
- No action is taken if `quarantine_threshold` is hit, only `reject_threshold`
applies.
- `defer_sender_reject` from SMTP configuration takes no effect.
- MAIL FROM is not checked, even if specified.
If you often get hit by spam attacks, it is recommended to enable this
setting to save server resources.
---
### quarantine_threshold _integer_
Default: `1`
DNSBL score needed (equals-or-higher) to quarantine the message.
---
### reject_threshold _integer_
Default: `9999`
DNSBL score needed (equals-or-higher) to reject the message.
## List configuration
```
dnsbl.example.org dnsbl.example.com {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
responses 127.0.0.1/24
score 1
}
```
Directive name and arguments specify the actual DNS zone to query when checking
the list. Using multiple arguments is equivalent to specifying the same
configuration separately for each list.
### client_ipv4 _boolean_
Default: `yes`
Whether to check address of the IPv4 clients against the list.
---
### client_ipv6 _boolean_
Default: `yes`
Whether to check address of the IPv6 clients against the list.
---
### ehlo _boolean_
Default: `no`
Whether to check hostname specified n the HELO/EHLO command
against the list.
This works correctly only with domain-based DNSBLs.
---
### mailfrom _boolean_
Default: `no`
Whether to check domain part of the MAIL FROM address against the list.
This works correctly only with domain-based DNSBLs.
---
### responses _cidr_ | _ip..._
Default: `127.0.0.1/24`
IP networks (in CIDR notation) or addresses to permit in list lookup results.
Addresses not matching any entry in this directives will be ignored.
---
### score _integer_
Default: `1`
Score value to add for the message if it is listed.
If sum of list scores is equals or higher than `quarantine_threshold`, the
message will be quarantined.
If sum of list scores is equals or higher than `rejected_threshold`, the message
will be rejected.
It is possible to specify a negative value to make list act like a whitelist
and override results of other blocklists.

View File

@ -0,0 +1,49 @@
# 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
### endpoint _scheme://path_
Default: not set
Specifies milter protocol endpoint to use.
The endpoit is specified in standard URL-like format:
`tcp://127.0.0.1:6669` or `unix:///var/lib/milter/filter.sock`
---
### fail_open _boolean_
Default: `false`
Toggles behavior on milter I/O errors. If false ("fail closed") - message is
rejected with temporary error code. If true ("fail open") - check is skipped.

View File

@ -0,0 +1,48 @@
# Misc checks
## Configuration directives
Following directives are defined for all modules listed below.
### fail_action `ignore` | `reject` | `quarantine`
Default: `quarantine`
Action to take when check fails. See [Check actions](../actions/) for details.
---
### debug _boolean_
Default: global directive value
Log both successful and unsuccessful check executions instead of just
unsuccessful.
---
### require_mx_record
Check that domain in MAIL FROM command does have a MX record and none of them
are "null" (contain a single dot as the host).
By default, quarantines messages coming from servers missing MX records,
use `fail_action` directive to change that.
---
### require_matching_rdns
Check that source server IP does have a PTR record point to the domain
specified in EHLO/HELO command.
By default, quarantines messages coming from servers with mismatched or missing
PTR record, use `fail_action` directive to change that.
---
### require_tls
Check that the source server is connected via TLS; either directly, or by using
the STARTTLS command.
By default, rejects messages coming from unencrypted servers. Use the
`fail_action` directive to change that.

View File

@ -0,0 +1,97 @@
# 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
### tls_client { ... }
Default: not set
Configure TLS client if HTTPS is used. See [TLS configuration / Client](/reference/tls/#client) for details.
---
### api_path _url_
Default: `http://127.0.0.1:11333`
URL of HTTP API endpoint. Supports both HTTP and HTTPS and can include
path element.
---
### settings_id _string_
Default: not set
Settings ID to pass to the server.
---
### tag _string_
Default: `maddy`
Value to send in MTA-Tag header field.
---
### hostname _string_ <br>
Default: value of global directive
Value to send in MTA-Name header field.
---
### io_error_action _action_
Default: `ignore`
Action to take in case of inability to contact the rspamd server.
---
### error_resp_action _action_
Default: `ignore`
Action to take in case of 5xx or 4xx response received from the rspamd server.
---
### add_header_action _action_
Default: `quarantine`
Action to take when rspamd requests to "add header".
X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.
---
### rewrite_subj_action _action_
Default: `quarantine`
Action to take when rspamd requests to "rewrite subject".
X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.
---
### flags _string-list..._
Default: `pass_all`
Flags to pass to the rspamd server.
See [https://rspamd.com/doc/architecture/protocol.html](https://rspamd.com/doc/architecture/protocol.html) for details.

View File

@ -0,0 +1,97 @@
# SPF
check.spf the check module that verifies whether IP address of the client is
authorized to send messages for domain in MAIL FROM address.
SPF statuses are mapped to maddy check actions in a way
specified by \*_action directives. By default, SPF failure
results in the message being quarantined and errors (both permanent and
temporary) cause message to be rejected.
Authentication-Results field is generated irregardless of status.
## DMARC override
It is recommended by the DMARC standard to don't fail delivery based solely on
SPF policy and always check DMARC policy and take action based on it.
If `enforce_early` is `no`, check.spf module will not take any action on SPF
policy failure if sender domain does have a DMARC record with 'quarantine' or
'reject' policy. Instead it will rely on DMARC support to take necesary
actions using SPF results as an input.
Disabling `enforce_early` without enabling DMARC support will make SPF policies
no-op and is considered insecure.
## Configuration directives
```
check.spf {
debug no
enforce_early no
fail_action quarantine
softfail_action ignore
permerr_action reject
temperr_action reject
}
```
### debug _boolean_
Default: global directive value
Enable verbose logging for check.spf.
---
### enforce_early _boolean_
Default: `no`
Make policy decision on MAIL FROM stage (before the message body is received).
This makes it impossible to apply DMARC override (see above).
---
### none_action `reject` | `quarantine` | `ignore`
Default: `ignore`
Action to take when SPF policy evaluates to a 'none' result.
See [https://tools.ietf.org/html/rfc7208#section-2.6](https://tools.ietf.org/html/rfc7208#section-2.6) for meaning of
SPF results.
---
### neutral_action `reject` | `quarantine` | `ignore`
Default: `ignore`
Action to take when SPF policy evaluates to a 'neutral' result.
See [https://tools.ietf.org/html/rfc7208#section-2.6](https://tools.ietf.org/html/rfc7208#section-2.6) for meaning of
SPF results.
---
### fail_action `reject` | `quarantine` | `ignore`
Default: `quarantine`
Action to take when SPF policy evaluates to a 'fail' result.
---
### softfail_action `reject` | `quarantine` | `ignore`
Default: `ignore`
Action to take when SPF policy evaluates to a 'softfail' result.
---
### permerr_action `reject` | `quarantine` | `ignore`
Default: `reject`
Action to take when SPF policy evaluates to a 'permerror' result.
---
### temperr_action `reject` | `quarantine` | `ignore`
Default: `reject`
Action to take when SPF policy evaluates to a 'temperror' result.

View File

@ -0,0 +1,200 @@
# 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!).

View File

@ -0,0 +1,164 @@
# IMAP4rev1 endpoint
Module 'imap' is a listener that implements IMAP4rev1 protocol and provides
access to local messages storage specified by 'storage' directive.
In most cases, local storage modules will auto-create accounts when they are
accessed via IMAP. This relies on authentication provider used by IMAP endpoint
to provide what essentially is access control. There is a caveat, however: this
auto-creation will not happen when delivering incoming messages via SMTP as
there is no authentication to confirm that this account should indeed be
created.
## Configuration directives
```
imap tcp://0.0.0.0:143 tls://0.0.0.0:993 {
tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
io_debug no
debug no
insecure_auth no
sasl_login no
auth pam
storage &local_mailboxes
auth_map identity
auth_map_normalize auto
storage_map identity
storage_map_normalize auto
}
```
### tls _certificate-path_ _key-path_ { ... }
Default: global directive value
TLS certificate & key to use. Fine-tuning of other TLS properties is possible
by specifying a configuration block and options inside it:
```
tls cert.crt key.key {
protocols tls1.2 tls1.3
}
```
See [TLS configuration / Server](/reference/tls/#server-side) for details.
---
### proxy_protocol _trusted ips..._ { ... }
Default: not enabled
Enable use of HAProxy PROXY protocol. Supports both v1 and v2 protocols.
If a list of trusted IP addresses or subnets is provided, only connections
from those will be trusted.
TLS for the channel between the proxies and maddy can be configured
using a 'tls' directive:
```
proxy_protocol {
trust 127.0.0.1 ::1 192.168.0.1/24
tls &proxy_tls
}
```
Note that the top-level 'tls' directive is not inherited here. If you
need TLS on top of the PROXY protocol, securing the protocol header,
you must declare TLS explicitly.
---
### io_debug _boolean_
Default: `no`
Write all commands and responses to stderr.
---
### io_errors _boolean_
Default: `no`
Log I/O errors.
---
### debug _boolean_
Default: global directive value
Enable verbose logging.
---
### insecure_auth _boolean_
Default: `no` (`yes` if TLS is disabled)
Allow plain-text authentication over unencrypted connections.
---
### sasl_login _boolean_
Default: `no`
Enable support for SASL LOGIN authentication mechanism used by
some outdated clients.
---
### auth _module-reference_
**Required.**
Use the specified module for authentication.
---
### storage _module-reference_
**Required.**
Use the specified module for message storage.
---
### storage_map _module-reference_
Default: `identity`
Use the specified table to map SASL usernames to storage account names.
Before username is looked up, it is normalized using function defined by
`storage_map_normalize`.
This directive is useful if you want users user@example.org and user@example.com
to share the same storage account named "user". In this case, use
```
storage_map email_localpart
```
Note that `storage_map` does not affect the username passed to the
authentication provider.
It also does not affect how message delivery is handled, you should specify
`delivery_map` in storage module to define how to map email addresses
to storage accounts. E.g.
```
storage.imapsql local_mailboxes {
...
delivery_map email_localpart # deliver "user@*" to mailbox for "user"
}
```
---
### storage_map_normalize _function_
Default: `auto`
Same as `auth_map_normalize` but for `storage_map`.
---
### auth_map_normalize _function_
Default: `auto`
Overrides global `auth_map_normalize` value for this endpoint.
See [Global configuration](/reference/global-config) for details.

View File

@ -0,0 +1,41 @@
# 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}
```

View File

@ -0,0 +1,312 @@
# SMTP/LMTP/Submission endpoint
Module 'smtp' is a listener that implements ESMTP protocol with optional
authentication, LMTP and Submission support. Incoming messages are processed in
accordance with pipeline rules (explained in Message pipeline section below).
```
smtp tcp://0.0.0.0:25 {
hostname example.org
tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
io_debug no
debug no
insecure_auth no
sasl_login no
read_timeout 10m
write_timeout 1m
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
### hostname _string_
Default: global directive value
Server name to use in SMTP banner.
```
220 example.org ESMTP Service Ready
```
---
### tls _certificate-path_ _key-path_ { ... }
Default: global directive value
TLS certificate & key to use. Fine-tuning of other TLS properties is possible
by specifying a configuration block and options inside it:
```
tls cert.crt key.key {
protocols tls1.2 tls1.3
}
```
See [TLS configuration / Server](/reference/tls/#server-side) for details.
---
### proxy_protocol _trusted ips..._ { ... } <br>
Default: not enabled
Enable use of HAProxy PROXY protocol. Supports both v1 and v2 protocols.
If a list of trusted IP addresses or subnets is provided, only connections
from those will be trusted.
TLS for the channel between the proxies and maddy can be configured
using a 'tls' directive:
```
proxy_protocol {
trust 127.0.0.1 ::1 192.168.0.1/24
tls &proxy_tls
}
```
---
### io_debug _boolean_
Default: `no`
Write all commands and responses to stderr.
---
### debug _boolean_
Default: global directive value
Enable verbose logging.
---
### insecure_auth _boolean_
Default: `no` (`yes` if TLS is disabled)
Allow plain-text authentication over unencrypted connections. Not recommended!
---
### sasl_login _boolean_
Default: `no`
Enable support for SASL LOGIN authentication mechanism used by
some outdated clients.
---
### read_timeout _duration_
Default: `10m`
I/O read timeout.
---
### write_timeout _duration_
Default: `1m`
I/O write timeout.
---
### max_message_size _size_
Default: `32M`
Limit the size of incoming messages to 'size'.
---
### max_header_size _size_
Default: `1M`
Limit the size of incoming message headers to 'size'.
---
### auth _module-reference_
Default: not specified
Use the specified module for authentication.
---
### defer_sender_reject _boolean_
Default: `yes`
Apply sender-based checks and routing logic when first RCPT TO command
is received. This allows maddy to log recipient address of the rejected
message and also improves interoperability with (improperly implemented)
clients that don't expect an error early in session.
---
### max_logged_rcpt_errors _integer_
Default: `5`
Amount of RCPT-time errors that should be logged. Further errors will be
handled silently. This is to prevent log flooding during email dictionary
attacks (address probing).
---
### max_received _integer_
Default: `50`
Max. amount of Received header fields in the message header. If the incoming
message has more fields than this number, it will be rejected with the permanent error
5.4.6 ("Routing loop detected").
---
### buffer `ram`<br>buffer `fs` _path_ <br>buffer `auto` _max-size_ _path_
Default: `auto 1M StateDirectory/buffer`
Temporary storage to use for the body of accepted messages.
- `ram` Store the body in RAM.
- `fs` Write out the message to the FS and read it back as needed.
_path_ can be omitted and defaults to StateDirectory/buffer.
- `auto` Store message bodies smaller than `_max_size_` entirely in RAM,
otherwise write them out to the FS. _path_ can be omitted and defaults to `StateDirectory/buffer`.
---
### smtp_max_line_length _integer_
Default: `4000`
The maximum line length allowed in the SMTP input stream. If client sends a
longer line - connection will be closed and message (if any) will be rejected
with a permanent error.
RFC 5321 has the recommended limit of 998 bytes. Servers are not required
to handle longer lines correctly but some senders may produce them.
Unless BDAT extension is used by the sender, this limitation also applies to
the message body.
---
### dmarc _boolean_
Default: `yes`
Enforce sender's DMARC policy. Due to implementation limitations, it is not a
check module.
**Note**: Report generation is not implemented now.
**Note**: DMARC needs SPF and DKIM checks to function correctly.
Without these, DMARC check will not run.
---
## Rate & concurrency limiting
### limits { ... }
Default: no limits
This allows configuring a set of message flow restrictions including
max. concurrency and rate per-endpoint, per-source, per-destination.
Limits are specified as directives inside the block:
```
limits {
all rate 20
destination concurrency 5
}
```
Supported limits:
### _scope_ rate _burst_ _period_
Rate limit. Restrict the amount of messages processed in _period_ to
_burst_ messages. If period is not specified, 1 second is used.
### _scope_ concurrency _max_
Concurrency limit. Restrict the amount of messages processed in parallel
to _max_.
For each supported limitation, _scope_ determines whether it should be applied
for all messages ("all"), per-sender IP ("ip"), per-sender domain ("source") or
per-recipient domain ("destination"). Having a scope other than "all" means
that the restriction will be enforced independently for each group determined
by scope. E.g. "ip rate 20" means that the same IP cannot send more than 20
messages per second. "destination concurrency 5" means that no more than 5
messages can be sent in parallel to a single domain.
**Note**: At the moment, SMTP endpoint on its own does not support per-recipient
limits. They will be no-op. If you want to enforce a per-recipient restriction
on outbound messages, do so using 'limits' directive for the 'table.remote' module
It is possible to share limit counters between multiple endpoints (or any other
modules). To do so define a top-level configuration block for module "limits"
and reference it where needed using standard & syntax. E.g.
```
limits inbound_limits {
all rate 20
}
smtp smtp://0.0.0.0:25 {
limits &inbound_limits
...
}
submission tls://0.0.0.0:465 {
limits &inbound_limits
...
}
```
Using an "all rate" restriction in such way means that no more than 20
messages can enter the server through both endpoints in one second.
# Submission module (submission)
Module 'submission' implements all functionality of the 'smtp' module and adds
certain message preprocessing on top of it, additionally authentication is
always required.
'submission' module checks whether addresses in header fields From, Sender, To,
Cc, Bcc, Reply-To are correct and adds Message-ID and Date if it is missing.
```
submission tcp://0.0.0.0:587 tls://0.0.0.0:465 {
# ... same as smtp ...
}
```
# LMTP module (lmtp)
Module 'lmtp' implements all functionality of the 'smtp' module but uses
LMTP (RFC 2033) protocol.
```
lmtp unix://lmtp.sock {
# ... same as smtp ...
}
```
## Limitations of LMTP implementation
- Can't be used with TCP.
- Delivery to 'sql' module storage is always atomic, either all recipients will
succeed or none of them will.

View File

@ -0,0 +1,153 @@
# Global configuration directives
These directives can be specified outside of any
configuration blocks and they are applied to all modules.
Some directives can be overridden on per-module basis (e.g. hostname).
### state_dir _path_
Default: `/var/lib/maddy`
The path to the state directory. This directory will be used to store all
persistent data and should be writable.
---
### runtime_dir _path_
Default: `/run/maddy`
The path to the runtime directory. Used for Unix sockets and other temporary
objects. Should be writable.
---
### hostname _domain_
Default: not specified
Internet hostname of this mail server. Typicall FQDN is used. It is recommended
to make sure domain specified here resolved to the public IP of the server.
---
### auth_map _module-reference_
Default: `identity`
Use the specified table to translate SASL usernames before passing it to the
authentication provider.
Before username is looked up, it is normalized using function defined by
`auth_map_normalize`.
Note that `auth_map` does not affect the storage account name used. You probably
should also use `storage_map` in IMAP config block to handle this.
This directive is useful if used authentication provider does not support
using emails as usernames but you still want users to have separate mailboxes
on separate domains. In this case, use it with `email_localpart` table:
```
auth_map email_localpart
```
With this configuration, `user@example.org` and `user@example.com` will use
`user` credentials when authenticating, but will access `user@example.org` and
`user@example.com` mailboxes correspondingly. If you want to also accept
`user` as a username, use `auth_map email_localpart_optional`.
If you want `user@example.org` and `user@example.com` to have the same mailbox,
also set `storage_map` in IMAP config block to use `email_localpart`
(or `email_localpart_optional` if you want to also accept just "user"):
```
storage_map email_localpart
```
In this case you will need to create storage accounts without domain part in
the name:
```
maddy imap-acct create user # instead of user@example.org
```
---
### auth_map_normalize _function_
Default: `auto`
Normalization function to apply to SASL usernames before mapping
them to storage accounts.
Available options:
- `auto` `precis_casefold_email` for valid emails, `precis_casefold` otherwise.
- `precis_casefold_email` PRECIS UsernameCaseMapped profile + U-labels form for domain
- `precis_casefold` PRECIS UsernameCaseMapped profile for the entire string
- `precis_email` PRECIS UsernameCasePreserved profile + U-labels form for domain
- `precis` PRECIS UsernameCasePreserved profile for the entire string
- `casefold` Convert to lower case
- `noop` Nothing
---
### autogenerated_msg_domain _domain_
Default: not specified
Domain that is used in From field for auto-generated messages (such as Delivery
Status Notifications).
---
### tls `file` _cert-file_ _pkey-file_ | _module-reference_ | `off`
Default: not specified
Default TLS certificate to use for all endpoints.
Must be present in either all endpoint modules configuration blocks or as
global directive.
You can also specify other configuration options such as cipher suites and TLS
version. See maddy-tls(5) for details. maddy uses reasonable
cipher suites and TLS versions by default so you generally don't have to worry
about it.
---
### tls_client { ... }
Default: not specified
This is optional block that specifies various TLS-related options to use when
making outbound connections. See TLS client configuration for details on
directives that can be used in it. maddy uses reasonable cipher suites and TLS
versions by default so you generally don't have to worry about it.
---
### log _targets..._ | `off`
Default: `stderr`
Write log to one of more "targets".
The target can be one or the following:
- `stderr` Write logs to stderr.
- `stderr_ts` Write logs to stderr with timestamps.
- `syslog` Send logs to the local syslog daemon.
- _file path_ Write (append) logs to file.
Example:
```
log syslog /var/log/maddy.log
```
**Note:** Maddy does not perform log files rotation, this is the job of the
logrotate daemon. Send SIGUSR1 to maddy process to make it reopen log files.
---
### debug _boolean_
Default: `no`
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.

View File

@ -0,0 +1,225 @@
# DKIM signing
modify.dkim module is a modifier that signs messages using DKIM
protocol (RFC 6376).
Each configuration block specifies a single selector
and one or more domains.
A key will be generated or read for each domain, the key to use
for each message will be selected based on the SMTP envelope sender. Exception
for that is that for domain-less postmaster address and null address, the
key for the first domain will be used. If domain in envelope sender
does not match any of loaded keys, message will not be signed.
Additionally, for each messages From header is checked to
match MAIL FROM and authorization identity (username sender is logged in as).
This can be controlled using require_sender_match directive.
Generated private keys are stored in unencrypted PKCS#8 format
in state_directory/dkim_keys (`/var/lib/maddy/dkim_keys`).
In the same directory .dns files are generated that contain
public key for each domain formatted in the form of a DNS record.
## Arguments
domains and selector can be specified in arguments, so actual modify.dkim use can
be shortened to the following:
```
modify {
dkim example.org selector
}
```
## Configuration directives
```
modify.dkim {
debug no
domains example.org example.com
selector default
key_path dkim-keys/{domain}-{selector}.key
oversign_fields ...
sign_fields ...
header_canon relaxed
body_canon relaxed
sig_expiry 120h # 5 days
hash sha256
newkey_algo rsa2048
}
```
### debug _boolean_
Default: global directive value
Enable verbose logging.
---
### domains _string-list_
**Required**. <br>
Default: not specified
ADministrative Management Domains (ADMDs) taking responsibility for messages.
Should be specified either as a directive or as an argument.
---
### selector _string_
**Required**. <br>
Default: not specified
Identifier of used key within the ADMD.
Should be specified either as a directive or as an argument.
---
### key_path _string_
Default: `dkim_keys/{domain}_{selector}.key`
Path to private key. It should be in PKCS#8 format wrapped in PAM encoding.
If key does not exist, it will be generated using algorithm specified
in newkey_algo.
Placeholders '{domain}' and '{selector}' will be replaced with corresponding
values from domain and selector directives.
Additionally, keys in PKCS#1 ("RSA PRIVATE KEY") and
RFC 5915 ("EC PRIVATE KEY") can be read by modify.dkim. Note, however that
newly generated keys are always in PKCS#8.
---
### oversign_fields _list..._
Default: see below
Header fields that should be signed n+1 times where n is times they are
present in the message. This makes it impossible to replace field
value by prepending another field with the same name to the message.
Fields specified here don't have to be also specified in `sign_fields`.
Default set of oversigned fields:
- Subject
- To
- From
- Date
- MIME-Version
- Content-Type
- Content-Transfer-Encoding
- Reply-To
- Message-Id
- References
- Autocrypt
- Openpgp
---
### sign_fields _list..._
Default: see below
Header fields that should be signed n times where n is times they are
present in the message. For these fields, additional values can be prepended
by intermediate relays, but existing values can't be changed.
Default set of signed fields:
- List-Id
- List-Help
- List-Unsubscribe
- List-Post
- List-Owner
- List-Archive
- Resent-To
- Resent-Sender
- Resent-Message-Id
- Resent-Date
- Resent-From
- Resent-Cc
---
### header_canon `relaxed` | `simple`
Default: `relaxed`
Canonicalization algorithm to use for header fields. With `relaxed`, whitespace within
fields can be modified without breaking the signature, with `simple` no
modifications are allowed.
---
### body_canon `relaxed` | `simple`
Default: `relaxed`
Canonicalization algorithm to use for message body. With `relaxed`, whitespace within
can be modified without breaking the signature, with `simple` no
modifications are allowed.
---
### sig_expiry _duration_
Default: `120h`
Time for which signature should be considered valid. Mainly used to prevent
unauthorized resending of old messages.
---
### hash _hash_
Default: `sha256`
Hash algorithm to use when computing body hash.
sha256 is the only supported algorithm now.
---
### newkey_algo `rsa4096` | `rsa2048` | `ed25519`
Default: `rsa2048`
Algorithm to use when generating a new key.
Currently ed25519 is **not** supported by most platforms.
---
### require_sender_match _ids..._
Default: `envelope auth`
Require specified identifiers to match From header field and key domain,
otherwise - don't sign the message.
If From field contains multiple addresses, message will not be
signed unless `allow_multiple_from` is also specified. In that
case only first address will be compared.
Matching is done in a case-insensitive way.
Valid values:
- `off` Disable check, always sign.
- `envelope` Require MAIL FROM address to match From header.
- `auth` If authorization identity contains @ - then require it to
fully match From header. Otherwise, check only local-part
(username).
---
### allow_multiple_from _boolean_
Default: `no`
Allow multiple addresses in From header field for purposes of
`require_sender_match` checks. Only first address will be checked, however.
---
### sign_subdomains _boolean_
Default: `no`
Sign emails from subdomains using a top domain key.
Allows only one domain to be specified (can be worked around by using `modify.dkim`
multiple times).

View File

@ -0,0 +1,63 @@
# 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
```

Some files were not shown because too many files have changed in this diff Show More