Merge branch 'dev'

This commit is contained in:
fox.cpp 2022-06-18 18:29:55 +03:00
commit 37bfe3bbd6
No known key found for this signature in database
GPG key ID: 5B991F6215D2FCC0
124 changed files with 6605 additions and 6017 deletions

View file

@ -1,36 +0,0 @@
image: archlinux
packages:
- go
- pam
- scdoc
- curl
sources:
- https://github.com/foxcpp/maddy
tasks:
- build: |
cd maddy
go build ./...
- buildsh: |
cd maddy
./build.sh
./build.sh --destdir destdir/ install
find destdir/
- test: |
cd maddy
go test ./... -coverprofile=coverage.out -covermode=atomic -race
- integration-test: |
cd maddy/tests
./run.sh
- lint: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.33.0
cd maddy/
$(go env GOPATH)/bin/golangci-lint run || true
- build-man-pages: |
cd maddy/docs/man
for f in *.scd; do scdoc < $f > /dev/null; done
- upload-coverage: |
export CODECOV_TOKEN=a4598288-4c29-4da7-87cf-64a36e23d245
cd maddy/
bash <(curl https://codecov.io/bash) -f coverage.out -F unit
cd tests/
bash <(curl https://codecov.io/bash) -f coverage.out -F integration

112
.github/workflows/cicd.yml vendored Normal file
View file

@ -0,0 +1,112 @@
name: "Testing and release preparation"
on:
push:
branches: [ master, dev ]
tags: [ "v*" ]
pull_request:
branches: [ master, dev ]
jobs:
build-and-test:
name: "Build and test"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: "Install libpam"
run: sudo apt-get install -y libpam-dev
- uses: actions/cache@v2
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- uses: actions/setup-go@v2
with:
go-version: 1.17.7
- name: "Verify build.sh"
run: |
./build.sh
./build.sh --destdir destdir/ install
find destdir/
- 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
artifact-builder:
name: "Prepare release artifacts"
needs: build-and-test
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
- name: "Create and package build tree"
run: |
./build.sh --static build
./build.sh --destdir ~/package-output/ install
mv ~/package-output/ ~/maddy-$(cat .version)-x86_64-linux-musl
pushd ~
tar c ./maddy-$(cat .version)-x86_64-linux-musl | zstd > ~/maddy-x86_64-linux-musl.tar.zst
popd
- name: "Save source tree"
run: |
rm -rf .git
cp -r . ~/maddy-$(cat .version)-src
pushd ~
tar c ./maddy-$(cat .version)-src | zstd > ~/maddy-src.tar.zst
popd
- name: "Upload source tree"
uses: actions/upload-artifact@v2
with:
name: maddy-src.tar.zst
path: '~/maddy-src.tar.zst'
if-no-files-found: error
- name: "Upload binary tree"
uses: actions/upload-artifact@v2
with:
name: maddy-binary.tar.zst
path: '~/maddy-x86_64-linux-musl.tar.zst'
if-no-files-found: error
docker-builder:
name: "Build Docker image"
needs: build-and-test
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
steps:
- name: "Login to Docker Hub"
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v1
with:
registry: "ghcr.io"
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Build and push"
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: |
foxcpp/maddy:${{ github.ref_name }}
ghcr.io/foxcpp/maddy:${{ github.ref_name }}

4
.gitignore vendored
View file

@ -24,8 +24,8 @@ _testmain.go
cmd/maddy/maddy
cmd/maddyctl/maddyctl
cmd/maddy-*-helper/maddy-*-helper
maddy
maddyctl
/maddy
/maddyctl
# Man pages
docs/man/*.1

View file

@ -2,44 +2,80 @@ site_name: maddy
repo_url: https://github.com/foxcpp/maddy
theme: readthedocs
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/'
- Integration with software:
- third-party/dovecot.md
- third-party/smtp-servers.md
- third-party/rspamd.md
- third-party/mailman3.md
- seclevels.md
- faq.md
- multiple-domains.md
- unicode.md
- upgrading.md
- specifications.md
- openmetrics.md
- Manual pages:
- man/_generated_maddy.1.md
- man/_generated_maddy.5.md
- man/_generated_maddy-auth.5.md
- man/_generated_maddy-blob.5.md
- man/_generated_maddy-config.5.md
- man/_generated_maddy-filters.5.md
- man/_generated_maddy-imap.5.md
- man/_generated_maddy-smtp.5.md
- man/_generated_maddy-storage.5.md
- man/_generated_maddy-targets.5.md
- man/_generated_maddy-tables.5.md
- man/_generated_maddy-tls.5.md
- seclevels.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/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/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
- internals/sqlite.md

View file

@ -1 +1 @@
0.5.4
0.6.0-dev

View file

@ -30,4 +30,4 @@ COPY --from=build-env /pkg/usr/local/bin/maddy /pkg/usr/local/bin/maddyctl /bin/
EXPOSE 25 143 993 587 465
VOLUME ["/data"]
ENTRYPOINT ["/bin/maddy", "-config", "/data/maddy.conf"]
ENTRYPOINT ["/bin/maddy", "-config", "/data/maddy.conf", "run"]

View file

@ -123,15 +123,9 @@ build() {
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
echo "-- Building management utility (maddyctl)..." >&2
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}/maddyctl" ${GOFLAGS} ./cmd/maddyctl
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
echo "-- Building management utility (maddyctl)..." >&2
go build -tags "$tags" -trimpath -ldflags="-X \"github.com/foxcpp/maddy.Version=${version}\"" -o "${builddir}/maddyctl" ${GOFLAGS} ./cmd/maddyctl
fi
build_man_pages
@ -147,7 +141,8 @@ install() {
echo "-- Installing built files..." >&2
command install -m 0755 -d "${destdir}/${prefix}/bin/"
command install -m 0755 "${builddir}/maddy" "${builddir}/maddyctl" "${destdir}/${prefix}/bin/"
command install -m 0755 "${builddir}/maddy" "${destdir}/${prefix}/bin/"
command ln -s maddy "${destdir}/${prefix}/bin/maddyctl"
command install -m 0755 -d "${destdir}/etc/maddy/"
command install -m 0644 ./maddy.conf "${destdir}/etc/maddy/maddy.conf"

View file

@ -5,14 +5,7 @@ maddy executables
Main server executable.
### maddyctl
IMAP index and authentication database inspection and manipulation utility.
### maddy-pam-helper, maddy-shadow-helper
__Deprecated: Currently they are unusable due to changes made to the storage
implementation.__
Utilities compatible with the auth.external module that call libpam or read
/etc/shadow on Unix systems.

View file

@ -19,11 +19,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"os"
"github.com/foxcpp/maddy"
_ "github.com/foxcpp/maddy"
"github.com/foxcpp/maddy/internal/cli"
_ "github.com/foxcpp/maddy/internal/cli/ctl"
)
func main() {
os.Exit(maddy.Run())
maddycli.Run()
}

View file

@ -1,511 +0,0 @@
/*
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 (
"bytes"
"errors"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/emersion/go-imap"
imapsql "github.com/foxcpp/go-imap-sql"
"github.com/foxcpp/maddy/cmd/maddyctl/clitools"
"github.com/foxcpp/maddy/framework/module"
"github.com/urfave/cli"
)
func FormatAddress(addr *imap.Address) string {
return fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName)
}
func FormatAddressList(addrs []*imap.Address) string {
res := make([]string, 0, len(addrs))
for _, addr := range addrs {
res = append(res, FormatAddress(addr))
}
return strings.Join(res, ", ")
}
func mboxesList(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
mboxes, err := u.ListMailboxes(ctx.Bool("subscribed,s"))
if err != nil {
return err
}
if len(mboxes) == 0 && !ctx.GlobalBool("quiet") {
fmt.Fprintln(os.Stderr, "No mailboxes.")
}
for _, mbox := range mboxes {
info, err := mbox.Info()
if err != nil {
return err
}
if len(info.Attributes) != 0 {
fmt.Print(info.Name, "\t", info.Attributes, "\n")
} else {
fmt.Println(info.Name)
}
}
return nil
}
func mboxesCreate(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
name := ctx.Args().Get(1)
if name == "" {
return errors.New("Error: NAME is required")
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
if ctx.IsSet("special") {
attr := "\\" + strings.Title(ctx.String("special"))
suu, ok := u.(SpecialUseUser)
if !ok {
return errors.New("Error: storage backend does not support SPECIAL-USE IMAP extension")
}
return suu.CreateMailboxSpecial(name, attr)
}
return u.CreateMailbox(name)
}
func mboxesRemove(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
name := ctx.Args().Get(1)
if name == "" {
return errors.New("Error: NAME is required")
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
mbox, err := u.GetMailbox(name)
if err != nil {
return err
}
if !ctx.Bool("yes,y") {
status, err := mbox.Status([]imap.StatusItem{imap.StatusMessages})
if err != nil {
return err
}
if status.Messages != 0 {
fmt.Fprintf(os.Stderr, "Mailbox %s contains %d messages.\n", name, status.Messages)
}
if !clitools.Confirmation("Are you sure you want to delete that mailbox?", false) {
return errors.New("Cancelled")
}
}
return u.DeleteMailbox(name)
}
func mboxesRename(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
oldName := ctx.Args().Get(1)
if oldName == "" {
return errors.New("Error: OLDNAME is required")
}
newName := ctx.Args().Get(2)
if newName == "" {
return errors.New("Error: NEWNAME is required")
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
return u.RenameMailbox(oldName, newName)
}
func msgsAdd(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
name := ctx.Args().Get(1)
if name == "" {
return errors.New("Error: MAILBOX is required")
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
mbox, err := u.GetMailbox(name)
if err != nil {
return err
}
flags := ctx.StringSlice("flag")
if flags == nil {
flags = []string{}
}
date := time.Now()
if ctx.IsSet("date") {
date = time.Unix(ctx.Int64("date"), 0)
}
buf := bytes.Buffer{}
if _, err := io.Copy(&buf, os.Stdin); err != nil {
return err
}
if buf.Len() == 0 {
return errors.New("Error: Empty message, refusing to continue")
}
status, err := mbox.Status([]imap.StatusItem{imap.StatusUidNext})
if err != nil {
return err
}
if err := mbox.CreateMessage(flags, date, &buf); err != nil {
return err
}
fmt.Println(status.UidNext)
return nil
}
func msgsRemove(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
name := ctx.Args().Get(1)
if name == "" {
return errors.New("Error: MAILBOX is required")
}
seqset := ctx.Args().Get(2)
if seqset == "" {
return errors.New("Error: SEQSET is required")
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
mbox, err := u.GetMailbox(name)
if err != nil {
return err
}
if !ctx.Bool("yes") {
if !clitools.Confirmation("Are you sure you want to delete these messages?", false) {
return errors.New("Cancelled")
}
}
mboxB := mbox.(*imapsql.Mailbox)
return mboxB.DelMessages(ctx.Bool("uid"), seq)
}
func msgsCopy(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
srcName := ctx.Args().Get(1)
if srcName == "" {
return errors.New("Error: SRCMAILBOX is required")
}
seqset := ctx.Args().Get(2)
if seqset == "" {
return errors.New("Error: SEQSET is required")
}
tgtName := ctx.Args().Get(3)
if tgtName == "" {
return errors.New("Error: TGTMAILBOX is required")
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
srcMbox, err := u.GetMailbox(srcName)
if err != nil {
return err
}
return srcMbox.CopyMessages(ctx.Bool("uid"), seq, tgtName)
}
func msgsMove(be module.Storage, ctx *cli.Context) error {
if ctx.Bool("y,yes") || !clitools.Confirmation("Currently, it is unsafe to remove messages from mailboxes used by connected clients, continue?", false) {
return errors.New("Cancelled")
}
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
srcName := ctx.Args().Get(1)
if srcName == "" {
return errors.New("Error: SRCMAILBOX is required")
}
seqset := ctx.Args().Get(2)
if seqset == "" {
return errors.New("Error: SEQSET is required")
}
tgtName := ctx.Args().Get(3)
if tgtName == "" {
return errors.New("Error: TGTMAILBOX is required")
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
srcMbox, err := u.GetMailbox(srcName)
if err != nil {
return err
}
moveMbox := srcMbox.(*imapsql.Mailbox)
return moveMbox.MoveMessages(ctx.Bool("uid"), seq, tgtName)
}
func msgsList(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
mboxName := ctx.Args().Get(1)
if mboxName == "" {
return errors.New("Error: MAILBOX is required")
}
seqset := ctx.Args().Get(2)
if seqset == "" {
seqset = "1:*"
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
mbox, err := u.GetMailbox(mboxName)
if err != nil {
return err
}
ch := make(chan *imap.Message, 10)
go func() {
err = mbox.ListMessages(ctx.Bool("uid"), seq, []imap.FetchItem{imap.FetchEnvelope, imap.FetchInternalDate, imap.FetchRFC822Size, imap.FetchFlags, imap.FetchUid}, ch)
}()
for msg := range ch {
if !ctx.Bool("full") {
fmt.Printf("UID %d: %s - %s\n %v, %v\n\n", msg.Uid, FormatAddressList(msg.Envelope.From), msg.Envelope.Subject, msg.Flags, msg.Envelope.Date)
continue
}
fmt.Println("- Server meta-data:")
fmt.Println("UID:", msg.Uid)
fmt.Println("Sequence number:", msg.SeqNum)
fmt.Println("Flags:", msg.Flags)
fmt.Println("Body size:", msg.Size)
fmt.Println("Internal date:", msg.InternalDate.Unix(), msg.InternalDate)
fmt.Println("- Envelope:")
if len(msg.Envelope.From) != 0 {
fmt.Println("From:", FormatAddressList(msg.Envelope.From))
}
if len(msg.Envelope.To) != 0 {
fmt.Println("To:", FormatAddressList(msg.Envelope.To))
}
if len(msg.Envelope.Cc) != 0 {
fmt.Println("CC:", FormatAddressList(msg.Envelope.Cc))
}
if len(msg.Envelope.Bcc) != 0 {
fmt.Println("BCC:", FormatAddressList(msg.Envelope.Bcc))
}
if msg.Envelope.InReplyTo != "" {
fmt.Println("In-Reply-To:", msg.Envelope.InReplyTo)
}
if msg.Envelope.MessageId != "" {
fmt.Println("Message-Id:", msg.Envelope.MessageId)
}
if !msg.Envelope.Date.IsZero() {
fmt.Println("Date:", msg.Envelope.Date.Unix(), msg.Envelope.Date)
}
if msg.Envelope.Subject != "" {
fmt.Println("Subject:", msg.Envelope.Subject)
}
fmt.Println()
}
return err
}
func msgsDump(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
mboxName := ctx.Args().Get(1)
if mboxName == "" {
return errors.New("Error: MAILBOX is required")
}
seqset := ctx.Args().Get(2)
if seqset == "" {
seqset = "*"
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
mbox, err := u.GetMailbox(mboxName)
if err != nil {
return err
}
ch := make(chan *imap.Message, 10)
go func() {
err = mbox.ListMessages(ctx.Bool("uid"), seq, []imap.FetchItem{imap.FetchRFC822}, ch)
}()
for msg := range ch {
for _, v := range msg.Body {
if _, err := io.Copy(os.Stdout, v); err != nil {
return err
}
}
}
return err
}
func msgsFlags(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
name := ctx.Args().Get(1)
if name == "" {
return errors.New("Error: MAILBOX is required")
}
seqStr := ctx.Args().Get(2)
if seqStr == "" {
return errors.New("Error: SEQ is required")
}
seq, err := imap.ParseSeqSet(seqStr)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
mbox, err := u.GetMailbox(name)
if err != nil {
return err
}
flags := ctx.Args()[3:]
if len(flags) == 0 {
return errors.New("Error: at least once FLAG is required")
}
var op imap.FlagsOp
switch ctx.Command.Name {
case "add-flags":
op = imap.AddFlags
case "rem-flags":
op = imap.RemoveFlags
case "set-flags":
op = imap.SetFlags
default:
panic("unknown command: " + ctx.Command.Name)
}
return mbox.UpdateMessagesFlags(ctx.IsSet("uid"), seq, op, flags)
}

View file

@ -1,136 +0,0 @@
/*
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 (
"errors"
"fmt"
"os"
specialuse "github.com/emersion/go-imap-specialuse"
"github.com/foxcpp/maddy/cmd/maddyctl/clitools"
"github.com/foxcpp/maddy/framework/module"
"github.com/urfave/cli"
)
type SpecialUseUser interface {
CreateMailboxSpecial(name, specialUseAttr string) error
}
func imapAcctList(be module.Storage, ctx *cli.Context) error {
mbe, ok := be.(module.ManageableStorage)
if !ok {
return errors.New("Error: storage backend does not support accounts management using maddyctl")
}
list, err := mbe.ListIMAPAccts()
if err != nil {
return err
}
if len(list) == 0 && !ctx.GlobalBool("quiet") {
fmt.Fprintln(os.Stderr, "No users.")
}
for _, user := range list {
fmt.Println(user)
}
return nil
}
func imapAcctCreate(be module.Storage, ctx *cli.Context) error {
mbe, ok := be.(module.ManageableStorage)
if !ok {
return errors.New("Error: storage backend does not support accounts management using maddyctl")
}
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
if err := mbe.CreateIMAPAcct(username); err != nil {
return err
}
act, err := mbe.GetIMAPAcct(username)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
suu, ok := act.(SpecialUseUser)
if !ok {
fmt.Fprintf(os.Stderr, "Note: Storage backend does not support SPECIAL-USE IMAP extension")
}
createMbox := func(name, specialUseAttr string) error {
if suu == nil {
return act.CreateMailbox(name)
}
return suu.CreateMailboxSpecial(name, specialUseAttr)
}
if name := ctx.String("sent-name"); name != "" {
if err := createMbox(name, specialuse.Sent); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create sent folder: %v", err)
}
}
if name := ctx.String("trash-name"); name != "" {
if err := createMbox(name, specialuse.Trash); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create trash folder: %v", err)
}
}
if name := ctx.String("junk-name"); name != "" {
if err := createMbox(name, specialuse.Junk); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create junk folder: %v", err)
}
}
if name := ctx.String("drafts-name"); name != "" {
if err := createMbox(name, specialuse.Drafts); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create drafts folder: %v", err)
}
}
if name := ctx.String("archive-name"); name != "" {
if err := createMbox(name, specialuse.Archive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create archive folder: %v", err)
}
}
return nil
}
func imapAcctRemove(be module.Storage, ctx *cli.Context) error {
mbe, ok := be.(module.ManageableStorage)
if !ok {
return errors.New("Error: storage backend does not support accounts management using maddyctl")
}
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
if !ctx.Bool("yes") {
if !clitools.Confirmation("Are you sure you want to delete this user account?", false) {
return errors.New("Cancelled")
}
}
return mbe.DeleteIMAPAcct(username)
}

View file

@ -1,792 +0,0 @@
/*
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 (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/foxcpp/maddy"
parser "github.com/foxcpp/maddy/framework/cfgparser"
"github.com/foxcpp/maddy/framework/config"
"github.com/foxcpp/maddy/framework/hooks"
"github.com/foxcpp/maddy/framework/module"
"github.com/foxcpp/maddy/internal/updatepipe"
"github.com/urfave/cli"
"golang.org/x/crypto/bcrypt"
)
func closeIfNeeded(i interface{}) {
if c, ok := i.(io.Closer); ok {
c.Close()
}
}
func main() {
app := cli.NewApp()
app.Name = "maddyctl"
app.Usage = "maddy mail server administration utility"
app.Version = maddy.BuildInfo()
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "config",
Usage: "Configuration file to use",
EnvVar: "MADDY_CONFIG",
Value: filepath.Join(maddy.ConfigDirectory, "maddy.conf"),
},
}
app.Commands = []cli.Command{
{
Name: "creds",
Usage: "Local credentials management",
Subcommands: []cli.Command{
{
Name: "list",
Usage: "List created credentials",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_authdb",
},
},
Action: func(ctx *cli.Context) error {
be, err := openUserDB(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return usersList(be, ctx)
},
},
{
Name: "create",
Usage: "Create user account",
Description: "Reads password from stdin",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_authdb",
},
cli.StringFlag{
Name: "password,p",
Usage: "Use `PASSWORD instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!",
},
cli.BoolFlag{
Name: "null,n",
Usage: "Create account with null password",
},
cli.StringFlag{
Name: "hash",
Usage: "Use specified hash algorithm. Valid values: sha3-512, bcrypt",
Value: "bcrypt",
},
cli.IntFlag{
Name: "bcrypt-cost",
Usage: "Specify bcrypt cost value",
Value: bcrypt.DefaultCost,
},
},
Action: func(ctx *cli.Context) error {
be, err := openUserDB(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return usersCreate(be, ctx)
},
},
{
Name: "remove",
Usage: "Delete user account",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_authdb",
},
cli.BoolFlag{
Name: "yes,y",
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openUserDB(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return usersRemove(be, ctx)
},
},
{
Name: "password",
Usage: "Change account password",
Description: "Reads password from stdin",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_authdb",
},
cli.StringFlag{
Name: "password,p",
Usage: "Use `PASSWORD` instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!",
},
},
Action: func(ctx *cli.Context) error {
be, err := openUserDB(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return usersPassword(be, ctx)
},
},
},
},
{
Name: "imap-acct",
Usage: "IMAP storage accounts management",
Subcommands: []cli.Command{
{
Name: "list",
Usage: "List storage accounts",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return imapAcctList(be, ctx)
},
},
{
Name: "create",
Usage: "Create IMAP storage account",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.StringFlag{
Name: "sent-name",
Usage: "Name of special mailbox for sent messages, use empty string to not create any",
Value: "Sent",
},
cli.StringFlag{
Name: "trash-name",
Usage: "Name of special mailbox for trash, use empty string to not create any",
Value: "Trash",
},
cli.StringFlag{
Name: "junk-name",
Usage: "Name of special mailbox for 'junk' (spam), use empty string to not create any",
Value: "Junk",
},
cli.StringFlag{
Name: "drafts-name",
Usage: "Name of special mailbox for drafts, use empty string to not create any",
Value: "Drafts",
},
cli.StringFlag{
Name: "archive-name",
Usage: "Name of special mailbox for archive, use empty string to not create any",
Value: "Archive",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return imapAcctCreate(be, ctx)
},
},
{
Name: "remove",
Usage: "Delete IMAP storage account",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "yes,y",
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return imapAcctRemove(be, ctx)
},
},
{
Name: "appendlimit",
Usage: "Query or set accounts's APPENDLIMIT value",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.IntFlag{
Name: "value,v",
Usage: "Set APPENDLIMIT to specified value (in bytes)",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return imapAcctAppendlimit(be, ctx)
},
},
},
},
{
Name: "imap-mboxes",
Usage: "IMAP mailboxes (folders) management",
Subcommands: []cli.Command{
{
Name: "list",
Usage: "Show mailboxes of user",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "subscribed,s",
Usage: "List only subscribed mailboxes",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return mboxesList(be, ctx)
},
},
{
Name: "create",
Usage: "Create mailbox",
ArgsUsage: "USERNAME NAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.StringFlag{
Name: "special",
Usage: "Set SPECIAL-USE attribute on mailbox; valid values: archive, drafts, junk, sent, trash",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return mboxesCreate(be, ctx)
},
},
{
Name: "remove",
Usage: "Remove mailbox",
Description: "WARNING: All contents of mailbox will be irrecoverably lost.",
ArgsUsage: "USERNAME MAILBOX",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "yes,y",
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return mboxesRemove(be, ctx)
},
},
{
Name: "rename",
Usage: "Rename mailbox",
Description: "Rename may cause unexpected failures on client-side so be careful.",
ArgsUsage: "USERNAME OLDNAME NEWNAME",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return mboxesRename(be, ctx)
},
},
},
},
{
Name: "imap-msgs",
Usage: "IMAP messages management",
Subcommands: []cli.Command{
{
Name: "add",
Usage: "Add message to mailbox",
ArgsUsage: "USERNAME MAILBOX",
Description: "Reads message body (with headers) from stdin. Prints UID of created message on success.",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.StringSliceFlag{
Name: "flag,f",
Usage: "Add flag to message. Can be specified multiple times",
},
cli.Int64Flag{
Name: "date,d",
Usage: "Set internal date value to specified UNIX timestamp",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsAdd(be, ctx)
},
},
{
Name: "add-flags",
Usage: "Add flags to messages",
ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...",
Description: "Add flags to all messages matched by SEQ.",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "uid,u",
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsFlags(be, ctx)
},
},
{
Name: "rem-flags",
Usage: "Remove flags from messages",
ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...",
Description: "Remove flags from all messages matched by SEQ.",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "uid,u",
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsFlags(be, ctx)
},
},
{
Name: "set-flags",
Usage: "Set flags on messages",
ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...",
Description: "Set flags on all messages matched by SEQ.",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "uid,u",
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsFlags(be, ctx)
},
},
{
Name: "remove",
Usage: "Remove messages from mailbox",
ArgsUsage: "USERNAME MAILBOX SEQSET",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "uid,u",
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
cli.BoolFlag{
Name: "yes,y",
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsRemove(be, ctx)
},
},
{
Name: "copy",
Usage: "Copy messages between mailboxes",
Description: "Note: You can't copy between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.",
ArgsUsage: "USERNAME SRCMAILBOX SEQSET TGTMAILBOX",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "uid,u",
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsCopy(be, ctx)
},
},
{
Name: "move",
Usage: "Move messages between mailboxes",
Description: "Note: You can't move between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.",
ArgsUsage: "USERNAME SRCMAILBOX SEQSET TGTMAILBOX",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "uid,u",
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
cli.BoolFlag{
Name: "yes,y",
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsMove(be, ctx)
},
},
{
Name: "list",
Usage: "List messages in mailbox",
Description: "If SEQSET is specified - only show messages that match it.",
ArgsUsage: "USERNAME MAILBOX [SEQSET]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "uid,u",
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
cli.BoolFlag{
Name: "full,f",
Usage: "Show entire envelope and all server meta-data",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsList(be, ctx)
},
},
{
Name: "dump",
Usage: "Dump message body",
Description: "If passed SEQ matches multiple messages - they will be joined.",
ArgsUsage: "USERNAME MAILBOX SEQ",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVar: "MADDY_CFGBLOCK",
Value: "local_mailboxes",
},
cli.BoolFlag{
Name: "uid,u",
Usage: "Use UIDs for SEQ instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsDump(be, ctx)
},
},
},
},
{
Name: "hash",
Usage: "Generate password hashes for use with pass_table",
Action: hashCommand,
Flags: []cli.Flag{
cli.StringFlag{
Name: "password,p",
Usage: "Use `PASSWORD instead of reading password from stdin\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!",
},
cli.StringFlag{
Name: "hash",
Usage: "Use specified hash algorithm",
Value: "bcrypt",
},
cli.IntFlag{
Name: "bcrypt-cost",
Usage: "Specify bcrypt cost value",
Value: bcrypt.DefaultCost,
},
cli.IntFlag{
Name: "argon2-time",
Usage: "Time factor for Argon2id",
Value: 3,
},
cli.IntFlag{
Name: "argon2-memory",
Usage: "Memory in KiB to use for Argon2id",
Value: 1024,
},
cli.IntFlag{
Name: "argon2-threads",
Usage: "Threads to use for Argon2id",
Value: 1,
},
},
},
}
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
func getCfgBlockModule(ctx *cli.Context) (map[string]interface{}, *maddy.ModInfo, error) {
cfgPath := ctx.GlobalString("config")
if cfgPath == "" {
return nil, nil, errors.New("Error: config is required")
}
cfgFile, err := os.Open(cfgPath)
if err != nil {
return nil, nil, fmt.Errorf("Error: failed to open config: %w", err)
}
defer cfgFile.Close()
cfgNodes, err := parser.Read(cfgFile, cfgFile.Name())
if err != nil {
return nil, nil, fmt.Errorf("Error: failed to parse config: %w", err)
}
globals, cfgNodes, err := maddy.ReadGlobals(cfgNodes)
if err != nil {
return nil, nil, err
}
if err := maddy.InitDirs(); err != nil {
return nil, nil, err
}
module.NoRun = true
_, mods, err := maddy.RegisterModules(globals, cfgNodes)
if err != nil {
return nil, nil, err
}
defer hooks.RunHooks(hooks.EventShutdown)
cfgBlock := ctx.String("cfg-block")
if cfgBlock == "" {
return nil, nil, errors.New("Error: cfg-block is required")
}
var mod maddy.ModInfo
for _, m := range mods {
if m.Instance.InstanceName() == cfgBlock {
mod = m
break
}
}
if mod.Instance == nil {
return nil, nil, fmt.Errorf("Error: unknown configuration block: %s", cfgBlock)
}
return globals, &mod, nil
}
func openStorage(ctx *cli.Context) (module.Storage, error) {
globals, mod, err := getCfgBlockModule(ctx)
if err != nil {
return nil, err
}
storage, ok := mod.Instance.(module.Storage)
if !ok {
return nil, fmt.Errorf("Error: configuration block %s is not an IMAP storage", ctx.String("cfg-block"))
}
if err := mod.Instance.Init(config.NewMap(globals, mod.Cfg)); err != nil {
return nil, fmt.Errorf("Error: module initialization failed: %w", err)
}
if updStore, ok := mod.Instance.(updatepipe.Backend); ok {
if err := updStore.EnableUpdatePipe(updatepipe.ModePush); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "Failed to initialize update pipe, do not remove messages from mailboxes open by clients: %v\n", err)
}
} else {
fmt.Fprintf(os.Stderr, "No update pipe support, do not remove messages from mailboxes open by clients\n")
}
return storage, nil
}
func openUserDB(ctx *cli.Context) (module.PlainUserDB, error) {
globals, mod, err := getCfgBlockModule(ctx)
if err != nil {
return nil, err
}
userDB, ok := mod.Instance.(module.PlainUserDB)
if !ok {
return nil, fmt.Errorf("Error: configuration block %s is not a local credentials store", ctx.String("cfg-block"))
}
if err := mod.Instance.Init(config.NewMap(globals, mod.Cfg)); err != nil {
return nil, fmt.Errorf("Error: module initialization failed: %w", err)
}
return userDB, nil
}

View file

@ -1,100 +0,0 @@
/*
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 (
"errors"
"fmt"
"os"
"github.com/foxcpp/maddy/cmd/maddyctl/clitools"
"github.com/foxcpp/maddy/framework/module"
"github.com/urfave/cli"
)
func usersList(be module.PlainUserDB, ctx *cli.Context) error {
list, err := be.ListUsers()
if err != nil {
return err
}
if len(list) == 0 && !ctx.GlobalBool("quiet") {
fmt.Fprintln(os.Stderr, "No users.")
}
for _, user := range list {
fmt.Println(user)
}
return nil
}
func usersCreate(be module.PlainUserDB, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
var pass string
if ctx.IsSet("password") {
pass = ctx.String("password")
} else {
var err error
pass, err = clitools.ReadPassword("Enter password for new user")
if err != nil {
return err
}
}
return be.CreateUser(username, pass)
}
func usersRemove(be module.PlainUserDB, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
if !ctx.Bool("yes") {
if !clitools.Confirmation("Are you sure you want to delete this user account?", false) {
return errors.New("Cancelled")
}
}
return be.DeleteUser(username)
}
func usersPassword(be module.PlainUserDB, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
var pass string
if ctx.IsSet("password") {
pass = ctx.String("password")
} else {
var err error
pass, err = clitools.ReadPassword("Enter new password")
if err != nil {
return err
}
}
return be.SetUserPassword(username, pass)
}

View file

@ -72,7 +72,7 @@ Restart=on-failure
# ... Unless it is a configuration problem.
RestartPreventExitStatus=2
ExecStart=/usr/local/bin/maddy
ExecStart=/usr/local/bin/maddy run
ExecReload=/bin/kill -USR1 $MAINPID
ExecReload=/bin/kill -USR2 $MAINPID

View file

@ -68,7 +68,7 @@ Restart=on-failure
# ... Unless it is a configuration problem.
RestartPreventExitStatus=2
ExecStart=/usr/local/bin/maddy -config /etc/maddy/%i.conf
ExecStart=/usr/local/bin/maddy --config /etc/maddy/%i.conf run
ExecReload=/bin/kill -USR1 $MAINPID
ExecReload=/bin/kill -USR2 $MAINPID

View file

@ -1,16 +1,42 @@
# Frequently Asked Questions
## Why?
## I configured maddy as recommended and gmail still puts my messages in spam
For fun. Turned out to be a rather convenient approach to
self-hosted email.
Unfortunately, GMail policies are opaque so we cannot tell why this happens.
## Is it caddy for email?
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.
No. It was intended to be one but developers quickly acknowledged
the fact email cannot be easily abstracted behind some magic.
Additionally, you may try marking multiple messages sent from
your domain as "not spam" in GMail UI.
## How it compares to MailCow or Mail-In-The-Box?
## 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
## maddyctl prints a "permission denied" error
Run maddyctl under the same user as maddy itself.
E.g.
```
sudo -u maddy maddyctl 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
@ -62,13 +88,6 @@ ZoneMTA has a number of features that may make it easier to integrate
with HTTP-based services. maddy speaks standard email protocols (SMTP,
Submission).
## What is the scope of project?
1. Implement a usable SMTP + Submission server that can both accept
and send email as secure as possible with todays state of
relevant protocols.
2. Implement a meaningful subset of IMAP for access to local storage.
## Is there a webmail?
No, at least currently.
@ -97,38 +116,4 @@ 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.
## Can I do X with maddy?
Ask on #maddy.
maddy is less feature-packed than other SMTP/IMAP server
implementations but it is not completely useless for anything other than
its default configuration.
## Can you implement X?
"Umbrella" projects like maddy are susceptible to scope
creep unless maintainers apply a lot of skepticism to proposed
features.
If X is essential for providing email security or extends the space of useful
configurations significantly and does not require major design changes -
we can talk, go to #maddy. Otherwise the likely answer is no.
## Are you breaking things between releases?
maddy releases follow Semantic Versioning 2.0.0 specification.
It is expected that 0.X releases may not be compatible with each
other. I attempt to minimize such breakage unless there is a significant
benefit.
## 1.0 when?
When no more backward-incompatible changes will be needed. maddy releases follow
Semantic Versioning 2.0.0 specification.
## maddy is bad name, it is almost impossible to Google!
Call it Maddy Mail Server.
together using off-the-shelf protocols.

View file

@ -1,373 +0,0 @@
maddy-auth(5) "maddy mail server" "maddy authentication backends"
; TITLE Authentication backends
# Introduction
Modules described in this man page can be used to provide functionality to
check validity of username-password pairs in accordance with some database.
That is, they authenticate users.
Most likely, you are going to use these modules with 'auth' directive of IMAP
(*maddy-imap*(5)) or SMTP endpoint (*maddy-smtp*(5)).
Most modules listed here are also usable as a table (see *maddy-tables*(5))
that contains all usernames known to the module. Exceptions are auth.external and
pam as underlying interfaces do not define a way to check credentials
existence.
# External authentication module (auth.external)
Module for authentication using external helper binary. It looks for binary
named maddy-auth-helper in $PATH and libexecdir and uses it for authentication
using username/password pair.
The protocol is very simple:
Program is launched for each authentication. Username and password are written
to stdin, adding \\n to the end. If binary exits with 0 status code -
authentication is considered successful. If the status code is 1 -
authentication is failed. If the status code is 2 - another unrelated error has
happened. Additional information should be written to stderr.
```
auth.external {
helper /usr/bin/ldap-helper
perdomain no
domains example.org
}
```
## Configuration directives
*Syntax*: helper _file_path_
Location of the helper binary. *Required.*
*Syntax*: perdomain _boolean_ ++
*Default*: no
Don't remove domain part of username when authenticating and require it to be
present. Can be used if you want user@domain1 and user@domain2 to be different
accounts.
*Syntax*: domains _domains..._ ++
*Default*: not specified
Domains that should be allowed in username during authentication.
For example, if 'domains' is set to "domain1 domain2", then
username, username@domain1 and username@domain2 will be accepted as valid login
name in addition to just username.
If used without 'perdomain', domain part will be removed from login before
check with underlying auth. mechanism. If 'perdomain' is set, then
domains must be also set and domain part WILL NOT be removed before check.
# PAM module (auth.pam)
Implements authentication using libpam. Alternatively it can be configured to
use helper binary like auth.external module does.
maddy should be built with libpam build tag to use this module without
'use_helper' directive.
```
go get -tags 'libpam' ...
```
```
auth.pam {
debug no
use_helper no
}
```
## Configuration directives
*Syntax*: debug _boolean_ ++
*Default*: no
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.
*Syntax*: use_helper _boolean_ ++
*Default*: no
Use LibexecDirectory/maddy-pam-helper instead of directly calling libpam.
You need to use that if:
1. maddy is not compiled with libpam, but maddy-pam-helper is built separately.
2. maddy is running as an unprivileged user and used PAM configuration requires additional
privileges (e.g. when using system accounts).
For 2, you need to make maddy-pam-helper binary setuid, see
README.md in source tree for details.
TL;DR (assuming you have the maddy group):
```
chown root:maddy /usr/lib/maddy/maddy-pam-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helper
```
# Shadow database authentication module (auth.shadow)
Implements authentication by reading /etc/shadow. Alternatively it can be
configured to use helper binary like auth.external does.
```
auth.shadow {
debug no
use_helper no
}
```
## Configuration directives
*Syntax*: debug _boolean_ ++
*Default*: no
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.
*Syntax*: use_helper _boolean_ ++
*Default*: no
Use LibexecDirectory/maddy-shadow-helper instead of directly reading /etc/shadow.
You need to use that if maddy is running as an unprivileged user
privileges (e.g. when using system accounts).
You need to make maddy-shadow-helper binary setuid, see
cmd/maddy-shadow-helper/README.md in source tree for details.
TL;DR (assuming you have maddy group):
```
chown root:maddy /usr/lib/maddy/maddy-shadow-helper
chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-shadow-helper
```
# Table-based password hash lookup (auth.pass_table)
This 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 (file module) or SQL query
(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 'maddyctl hash' command to generate suitable values.
See 'maddyctl hash --help' for details.
## maddyctl creds
If the underlying table is a "mutable" table (see maddy-tables(5)) then
the 'maddyctl creds' command can be used to modify the underlying tables
via pass_table module. It will act a "local credentials store" and will write
appropriate hash values to the table.
# Separate username and password lookup (auth.plain_separate)
This module implements authentication using username:password pairs but can
use zero or more "table modules" (maddy-tables(5)) and one or more
authentication providers to verify credentials.
```
auth.plain_separate {
user ...
user ...
...
pass ...
pass ...
...
}
```
How it works:
- Initial username input is normalized using PRECIS UsernameCaseMapped profile.
- Each table specified with the 'user' directive looked up using normalized
username. If match is not found in any table, authentication fails.
- Each authentication provider specified with the 'pass' directive is tried.
If authentication with all providers fails - an error is returned.
## Configuration directives
**Syntax:** user _table module_
Configuration block for any module from maddy-tables(5) can be used here.
Example:
```
user file /etc/maddy/allowed_users
```
**Syntax:** pass _auth provider_
Configuration block for any auth. provider module can be used here, even
'plain_split' itself.
The used auth. provider must provide username:password pair-based
authentication.
# Dovecot authentication client (auth.dovecot_sasl)
The 'dovecot_sasl' module implements the client side of the Dovecot
authentication protocol, allowing maddy to use it as a credentials source.
Currently SASL mechanisms support is limited to mechanisms supported by maddy
so you cannot get e.g. SCRAM-MD5 this way.
```
auth.dovecot_sasl {
endpoint unix://socket_path
}
dovecot_sasl unix://socket_path
```
## Configuration directives
*Syntax*: endpoint _schema://address_ ++
*Default*: not set
Set the address to use to contact Dovecot SASL server in the standard endpoint
format.
tcp://10.0.0.1:2222 for TCP, unix:///var/lib/dovecot/auth.sock for Unix
domain sockets.
# LDAP BindDN authentication (EXPERIMENTAL) (auth.ldap)
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 *maddy-storage*(5)).
auth.ldap also can be a used as a table module. This way you can check
whether the account exists. It works only if DN template is not used.
```
auth.ldap {
urls ldap://maddy.test:389
# Specify initial bind credentials. Not required ('bind off')
# if DN template is used.
bind plain "cn=maddy,ou=people,dc=maddy,dc=test" "123456"
# Specify DN template to skip lookup.
dn_template "cn={username},ou=people,dc=maddy,dc=test"
# Specify base_dn and filter to lookup DN.
base_dn "ou=people,dc=maddy,dc=test"
filter "(&(objectClass=posixAccount)(uid={username}))"
tls_client { ... }
starttls off
debug off
connect_timeout 1m
}
```
```
auth.ldap ldap://maddy.test.389 {
...
}
```
## Configuration directives
*Syntax:* urls _servers..._
REQUIRED.
URLs of the directory servers to use. First available server
is used - no load-balancing is done.
URLs should use 'ldap://', 'ldaps://', 'ldapi://' schemes.
*Syntax:* bind off ++
bind unauth ++
bind external ++
bind plain _username_ _password_ ++
*Default:* off
Credentials to use for initial binding. Required if DN lookup is used.
'unauth' performs unauthenticated bind. 'external' performs external binding
which is useful for Unix socket connections (ldapi://) or TLS client certificate
authentication (cert. is set using tls_client directive). 'plain' performs a
simple bind using provided credentials.
*Syntax:* dn_template _template_
DN template to use for binding. '{username}' is replaced with the
username specified by the user.
*Syntax:* base_dn _dn_
Base DN to use for lookup.
*Syntax:* filter _str_
DN lookup filter. '{username}' is replaced with the username specified
by the user.
Example:
```
(&(objectClass=posixAccount)(uid={username}))
```
Example (using ActiveDirectory):
```
(&(objectCategory=Person)(memberOf=CN=user-group,OU=example,DC=example,DC=org)(sAMAccountName={username})(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))
```
Example:
```
(&(objectClass=Person)(mail={username}))
```
*Syntax:* starttls _bool_ ++
*Default:* off
Whether to upgrade connection to TLS using STARTTLS.
*Syntax:* tls_client { ... }
Advanced TLS client configuration. See *maddy-tls*(5) for details.
*Syntax:* connect_timeout _duration_ ++
*Default:* 1m
Timeout for initial connection to the directory server.
*Syntax:* request_timeout _duration_ ++
*Default:* 1m
Timeout for each request (binding, lookup).

View file

@ -1,113 +0,0 @@
maddy-blob(5) "maddy mail server" "maddy reference documentation"
; TITLE Message blob storage
Some IMAP storage backends support pluggable message storage that allows
message contents to be stored separately from IMAP index.
Modules described in this page are what can be used with such storage backends.
In most cases they have to be specified using the 'msg_store' directive, like
this:
```
storage.imapsql local_mailboxes {
msg_store fs /var/lib/email
}
```
Unless explicitly configured, storage backends with pluggable storage will
store messages in state_dir/messages (e.g. /var/lib/maddy/messages) FS
directory.
# FS directory storage (storage.blob.fs)
This module stores message bodies in a file system directory.
```
storage.blob.fs {
root <directory>
}
```
```
storage.blob.fs <directory>
```
## Configuration directives
*Syntax:* root _path_ ++
*Default:* not set
Path to the FS directory. Must be readable and writable by the server process.
If it does not exist - it will be created (parent directory should be writable
for this). Relative paths are interpreted relatively to server state directory.
# Amazon S3 storage (storage.blob.s3)
This modules 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/
}
```
Example:
```
storage.imapsql local_mailboxes {
...
msg_store s3 {
endpoint s3.amazonaws.com
access_key "..."
secret_key "..."
bucket maddy-messages
region us-west-2
}
}
```
## Configuration directives
*Syntax:* endpoint _address:port_
REQUIRED.
Root S3 endpoint. e.g. s3.amazonaws.com
*Syntax:* secure _boolean_ ++
*Default:* yes
Whether TLS should be used.
*Syntax:* access_key _string_ ++
*Syntax:* secret_key _string_
REQUIRED.
Static S3 credentials.
*Syntax:* bucket _name_
REQUIRED.
S3 bucket name. The bucket must exist and
be read-writable.
*Syntax:* region _string_ ++
*Default:* not set
S3 bucket location. May be called "endpoint"
in some manuals.
*Syntax:* object_prefix _string_ ++
*Default:* empty string
String to add to all keys stored by maddy.
Can be useful when S3 is used as a file system.

View file

@ -1,953 +0,0 @@
maddy-filters(5) "maddy mail server" "maddy reference documentation"
; TITLE Message filtering
maddy does have two distinct types of modules that do message filtering.
"Checks" and "modifiers".
"Checks" are meant to be used to reject or quarantine
messages that are unwanted, such as potential spam or messages with spoofed
sender address. They are limited in ways they can modify the message and their
execution is heavily parallelized to improve performance.
"Modifiers" are executed serially in order they are referenced in the
configuration and are allowed to modify the message data and meta-data.
# 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 'remote' module (see *maddy-targets*(5))
will refuse to send quarantined messages.
# Simple checks
## Configuration directives
Following directives are defined for all modules listed below.
*Syntax*: ++
fail_action ignore ++
fail_action reject ++
fail_action quarantine ++
*Default*: quarantine
Action to take when check fails. See Check actions for details.
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Log both sucessfull and unsucessfull check executions instead of just
unsucessfull.
## require_mx_record
Check that domain in MAIL FROM command does have a MX record and none of them
are "null" (contain a single dot as the host).
By default, quarantines messages coming from servers missing MX records,
use 'fail_action' directive to change that.
## require_matching_rdns
Check that source server IP does have a PTR record point to the domain
specified in EHLO/HELO command.
By default, quarantines messages coming from servers with mismatched or missing
PTR record, use 'fail_action' directive to change that.
## require_tls
Check that the source server is connected via TLS; either directly, or by using
the STARTTLS command.
By default, rejects messages coming from unencrypted servers. Use the
'fail_action' directive to change that.
# DKIM authentication module (check.dkim)
This is the check module that performs verification of the DKIM signatures
present on the incoming messages.
```
check.dkim {
debug no
required_fields From Subject
allow_body_subset no
no_sig_action ignore
broken_sig_action ignore
fail_open no
}
```
## Configuration directives
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Log both sucessfull and unsucessfull check executions instead of just
unsucessfull.
*Syntax*: required_fields _string..._ ++
*Default*: From Subject
Header fields that should be included in each signature. If signature
lacks any field listed in that directive, it will be considered invalid.
Note that From is always required to be signed, even if it is not included in
this directive.
*Syntax*: no_sig_action _action_ ++
*Default*: ignore (recommended by RFC 6376)
Action to take when message without any signature is received.
Note that DMARC policy of the sender domain can request more strict handling of
missing DKIM signatures.
*Syntax*: broken_sig_action _action_ ++
*Default*: ignore (recommended by RFC 6376)
Action to take when there are not valid signatures in a message.
Note that DMARC policy of the sender domain can request more strict handling of
broken DKIM signatures.
*Syntax*: fail_open _boolean_ ++
*Default*: no
Whether to accept the message if a temporary error occurs during DKIM
verification. Rejecting the message with a 4xx code will require the sender
to resend it later in a hope that the problem will be resolved.
# SPF policy enforcement module (check.spf)
This is the check module that verifies whether IP address of the client is
authorized to send messages for domain in MAIL FROM address.
```
check.spf {
debug no
enforce_early no
fail_action quarantine
softfail_action ignore
permerr_action reject
temperr_action reject
}
```
## 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
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Enable verbose logging for check.spf.
*Syntax*: enforce_early _boolean_ ++
*Default*: no
Make policy decision on MAIL FROM stage (before the message body is received).
This makes it impossible to apply DMARC override (see above).
*Syntax*: none_action reject|qurantine|ignore ++
*Default*: ignore
Action to take when SPF policy evaluates to a 'none' result.
See https://tools.ietf.org/html/rfc7208#section-2.6 for meaning of
SPF results.
*Syntax*: neutral_action reject|qurantine|ignore ++
*Default*: ignore
Action to take when SPF policy evaluates to a 'neutral' result.
See https://tools.ietf.org/html/rfc7208#section-2.6 for meaning of
SPF results.
*Syntax*: fail_action reject|qurantine|ignore ++
*Default*: quarantine
Action to take when SPF policy evaluates to a 'fail' result.
*Syntax*: softfail_action reject|qurantine|ignore ++
*Default*: ignore
Action to take when SPF policy evaluates to a 'softfail' result.
*Syntax*: permerr_action reject|qurantine|ignore ++
*Default*: reject
Action to take when SPF policy evaluates to a 'permerror' result.
*Syntax*: temperr_action reject|qurantine|ignore ++
*Default*: reject
Action to take when SPF policy evaluates to a 'temperror' result.
# DNSBL lookup module (check.dnsbl)
The dnsbl module implements checking of source IP and hostnames against a set
of DNS-based Blackhole lists (DNSBLs).
Its configuration consists of module configuration directives and a set
of blocks specifing lists to use and kind of lookups to perform on them.
```
check.dnsbl {
debug no
check_early no
quarantine_threshold 1
reject_threshold 1
# Lists configuration example.
dnsbl.example.org {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
score 1
}
hsrbl.example.org {
client_ipv4 no
client_ipv6 no
ehlo yes
mailfrom yes
score 1
}
}
```
## Arguments
Arguments specify the list of IP-based BLs to use.
The following configurations are equivalent.
```
check {
dnsbl dnsbl.example.org dnsbl2.example.org
}
```
```
check {
dnsbl {
dnsbl.example.org dnsbl2.example.org {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
score 1
}
}
}
```
## Configuration directives
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Enable verbose logging.
*Syntax*: check_early _boolean_ ++
*Default*: no
Check BLs before mail delivery starts and silently reject blacklisted clients.
For this to work correctly, check should not be used in source/destination
pipeline block.
In particular, this means:
- No logging is done for rejected messages.
- No action is taken if quarantine_threshold is hit, only reject_threshold
applies.
- defer_sender_reject from SMTP configuration takes no effect.
- MAIL FROM is not checked, even if specified.
If you often get hit by spam attacks, this is recommended to enable this
setting to save server resources.
*Syntax*: quarantine_threshold _integer_ ++
*Default*: 1
DNSBL score needed (equals-or-higher) to quarantine the message.
*Syntax*: reject_threshold _integer_ ++
*Default*: 9999
DNSBL score needed (equals-or-higher) to reject the message.
## List configuration
```
dnsbl.example.org dnsbl.example.com {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
responses 127.0.0.1/24
score 1
}
```
Directive name and arguments specify the actual DNS zone to query when checking
the list. Using multiple arguments is equivalent to specifying the same
configuration separately for each list.
*Syntax*: client_ipv4 _boolean_ ++
*Default*: yes
Whether to check address of the IPv4 clients against the list.
*Syntax*: client_ipv6 _boolean_ ++
*Default*: yes
Whether to check address of the IPv6 clients against the list.
*Syntax*: ehlo _boolean_ ++
*Default*: no
Whether to check hostname specified n the HELO/EHLO command
against the list.
This works correctly only with domain-based DNSBLs.
*Syntax*: mailfrom _boolean_ ++
*Default*: no
Whether to check domain part of the MAIL FROM address against the list.
This works correctly only with domain-based DNSBLs.
*Syntax*: responses _cidr|ip..._ ++
*Default*: 127.0.0.1/24
IP networks (in CIDR notation) or addresses to permit in list lookup results.
Addresses not matching any entry in this directives will be ignored.
*Syntax*: score _integer_ ++
*Default*: 1
Score value to add for the message if it is listed.
If sum of list scores is equals or higher than quarantine_threshold, the
message will be quarantined.
If sum of list scores is equals or higher than rejected_threshold, the message
will be rejected.
It is possible to specify a negative value to make list act like a whitelist
and override results of other blocklists.
# DKIM signing module (modify.dkim)
modify.dkim module is a modifier that signs messages using DKIM
protocol (RFC 6376).
```
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
}
```
## 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
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Enable verbose logging.
*Syntax*: domains _string list_ ++
*Default*: not specified
*REQUIRED.*
ADministrative Management Domains (ADMDs) taking responsibility for messages.
A key will be generated or read for each domain specified here, 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.
Should be specified either as a directive or as an argument.
*Syntax*: selector _string_ ++
*Default*: not specified
*REQUIRED.*
Identifier of used key within the ADMD.
Should be specified either as a directive or as an argument.
*Syntax*: key_path _string_ ++
*Default*: dkim_keys/{domain}\_{selector}.key
Path to private key. It should be in PKCS#8 format wrapped in PAM encoding.
If key does not exist, it will be generated using algorithm specified
in newkey_algo.
Placeholders '{domain}' and '{selector}' will be replaced with corresponding
values from domain and selector directives.
Additionally, keys in PKCS#1 ("RSA PRIVATE KEY") and
RFC 5915 ("EC PRIVATE KEY") can be read by modify.dkim. Note, however that
newly generated keys are always in PKCS#8.
*Syntax*: oversign_fields _list..._ ++
*Default*: see below
Header fields that should be signed n+1 times where n is times they are
present in the message. This makes it impossible to replace field
value by prepending another field with the same name to the message.
Fields specified here don't have to be also specified in sign_fields.
Default set of oversigned fields:
- Subject
- To
- From
- Date
- MIME-Version
- Content-Type
- Content-Transfer-Encoding
- Reply-To
- Message-Id
- References
- Autocrypt
- Openpgp
*Syntax*: sign_fields _list..._ ++
*Default*: see below
Header fields that should be signed n+1 times where n is times they are
present in the message. For these fields, additional values can be prepended
by intermediate relays, but existing values can't be changed.
Default set of signed fields:
- List-Id
- List-Help
- List-Unsubscribe
- List-Post
- List-Owner
- List-Archive
- Resent-To
- Resent-Sender
- Resent-Message-Id
- Resent-Date
- Resent-From
- Resent-Cc
*Syntax*: header_canon relaxed|simple ++
*Default*: relaxed
Canonicalization algorithm to use for header fields. With 'relaxed', whitespace within
fields can be modified without breaking the signature, with 'simple' no
modifications are allowed.
*Syntax*: body_canon relaxed|simple ++
*Default*: relaxed
Canonicalization algorithm to use for message body. With 'relaxed', whitespace within
can be modified without breaking the signature, with 'simple' no
modifications are allowed.
*Syntax*: sig_expiry _duration_ ++
*Default*: 120h
Time for which signature should be considered valid. Mainly used to prevent
unauthorized resending of old messages.
*Syntax*: hash _hash_ ++
*Default*: sha256
Hash algorithm to use when computing body hash.
sha256 is the only supported algorithm now.
*Syntax*: newkey_algo rsa4096|rsa2048|ed25519 ++
*Default*: rsa2048
Algorithm to use when generating a new key.
*Syntax*: require_sender_match _ids..._ ++
*Default*: envelope auth
Require specified identifiers to match From header field and key domain,
otherwise - don't sign the message.
If From field contains multiple addresses, message will not be
signed unless allow_multiple_from is also specified. In that
case only first address will be compared.
Matching is done in a case-insensitive way.
Valid values:
- off +
Disable check, always sign.
- envelope +
Require MAIL FROM address to match From header.
- auth +
If authorization identity contains @ - then require it to
fully match From header. Otherwise, check only local-part
(username).
*Syntax*: allow_multiple_from _boolean_ ++
*Default*: no
Allow multiple addresses in From header field for purposes of
require_sender_match checks. Only first address will be checked, however.
*Syntax*: sign_subdomains _boolean_ ++
*Default*: no
Sign emails from subdomains using a top domain key.
Allows only one domain to be specified (can be workarounded using modify.dkim
multiple times).
# Envelope sender / recipient rewriting (modify.replace_sender, modify.replace_rcpt)
'replace_sender' and 'replace_rcpt' modules replace SMTP envelope addresses
based on the mapping defined by the table module (maddy-tables(5)). Currently,
only 1:1 mappings are supported (that is, it is not possible to specify
multiple replacements for a single address).
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
}
replace_rcpt regexp "(.+)@example.net" "$1@example.org"
}
```
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
```
# System command filter (check.command)
This module executes an arbitrary system command during a specified stage of
checks execution.
```
command executable_name arg0 arg1 ... {
run_on body
code 1 reject
code 2 quarantine
}
```
## Arguments
The module arguments specify the command to run. If the first argument is not
an absolute path, it is looked up in the Libexec Directory (/usr/lib/maddy on
Linux) and in $PATH (in that ordering). Note that no additional handling
of arguments is done, especially, the command is executed directly, not via the
system shell.
There is a set of special strings that are replaced with the corresponding
message-specific values:
- {source_ip}
IPv4/IPv6 address of the sending MTA.
- {source_host}
Hostname of the sending MTA, from the HELO/EHLO command.
- {source_rdns}
PTR record of the sending MTA IP address.
- {msg_id}
Internal message identifier. Unique for each delivery.
- {auth_user}
Client username, if authenticated using SASL PLAIN
- {sender}
Message sender address, as specified in the MAIL FROM SMTP command.
- {rcpts}
List of accepted recipient addresses, including the currently handled
one.
- {address}
Currently handled address. This is a recipient address if the command
is called during RCPT TO command handling ('run_on rcpt') or a sender
address if the command is called during MAIL FROM command handling ('run_on
sender').
If value is undefined (e.g. {source_ip} for a message accepted over a Unix
socket) or unavailable (the command is executed too early), the placeholder
is replaced with an empty string. Note that it can not remove the argument.
E.g. -i {source_ip} will not become just -i, it will be -i ""
Undefined placeholders are not replaced.
## Command stdout
The command stdout must be either empty or contain a valid RFC 5322 header.
If it contains a byte stream that does not look a valid header, the message
will be rejected with a temporary error.
The header from stdout will be *prepended* to the message header.
## Configuration directives
*Syntax*: run_on conn|sender|rcpt|body ++
*Default*: body
When to run the command. This directive also affects the information visible
for the message.
- conn
Run before the sender address (MAIL FROM) is handled.
*Stdin*: Empty ++
*Available placeholders*: {source_ip}, {source_host}, {msg_id}, {auth_user}.
- sender
Run during sender address (MAIL FROM) handling.
*Stdin*: Empty ++
*Available placeholders*: conn placeholders + {sender}, {address}.
The {address} placeholder contains the MAIL FROM address.
- rcpt
Run during recipient address (RCPT TO) handling. The command is executed
once for each RCPT TO command, even if the same recipient is specified
multiple times.
*Stdin*: Empty ++
*Available placeholders*: sender placeholders + {rcpts}.
The {address} placeholder contains the recipient address.
- body
Run during message body handling.
*Stdin*: The message header + body ++
*Available placeholders*: all except for {address}.
*Syntax*: ++
code _integer_ ignore ++
code _integer_ quarantine ++
code _integer_ reject [SMTP code] [SMTP enhanced code] [SMTP message]
This directives specified the mapping from the command exit code _integer_ to
the message pipeline action.
Two codes are defined implicitly, exit code 1 causes the message to be rejected
with a permanent error, exit code 2 causes the message to be quarantined. Both
action can be overriden using the 'code' directive.
## Milter protocol check (check.milter)
The 'milter' implements subset of Sendmail's milter protocol that can be used
to integrate external software in maddy.
Notable limitations of protocol implementation in maddy include:
1. Changes of envelope sender address are not supported
2. Removal and addition of envelope recipients is not supported
3. Removal and replacement of header fields is not supported
4. Headers fields can be inserted only on top
5. Milter does not receive some "macros" provided by sendmail.
Restrictions 1 and 2 are inherent to the maddy checks interface and cannot be
removed without major changes to it. Restrictions 3, 4 and 5 are temporary due to
incomplete implementation.
```
check.milter {
endpoint <endpoint>
fail_open false
}
milter <endpoint>
```
## Arguments
When defined inline, the first argument specifies endpoint to access milter
via. See below.
## Configuration directives
**Syntax:** endpoint _scheme://path_ ++
**Default:** not set
Specifies milter protocol endpoint to use.
The endpoit is specified in standard URL-like format:
'tcp://127.0.0.1:6669' or 'unix:///var/lib/milter/filter.sock'
**Syntax:** fail_open _boolean_ ++
**Default:** false
Toggles behavior on milter I/O errors. If false ("fail closed") - message is
rejected with temporary error code. If true ("fail open") - check is skipped.
## rspamd check (check.rspamd)
The 'rspamd' module implements message filtering by contacting the rspamd
server via HTTP API.
```
check.rspamd {
tls_client { ... }
api_path http://127.0.0.1:11333
settings_id whatever
tag maddy
hostname mx.example.org
io_error_action ignore
error_resp_action ignore
add_header_action quarantine
rewrite_subj_action quarantine
flags pass_all
}
rspamd http://127.0.0.1:11333
```
## Configuration directives
*Syntax:* tls_client { ... } ++
*Default:* not set
Configure TLS client if HTTPS is used, see *maddy-tls*(5) for details.
*Syntax:* api_path _url_ ++
*Default:* http://127.0.0.1:11333
URL of HTTP API endpoint. Supports both HTTP and HTTPS and can include
path element.
*Syntax:* settings_id _string_ ++
*Default:* not set
Settings ID to pass to the server.
*Syntax:* tag _string_ ++
*Default:* maddy
Value to send in MTA-Tag header field.
*Syntax:* hostname _string_ ++
*Default:* value of global directive
Value to send in MTA-Name header field.
*Syntax:* io_error_action _action_ ++
*Default:* ignore
Action to take in case of inability to contact the rspamd server.
*Syntax:* error_resp_action _action_ ++
*Default:* ignore
Action to take in case of 5xx or 4xx response received from the rspamd server.
*Syntax:* add_header_action _action_ ++
*Default:* quarantine
Action to take when rspamd requests to "add header".
X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.
*Syntax:* rewrite_subj_action _action_ ++
*Default:* quarantine
Action to take when rspamd requests to "rewrite subject".
X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.
*Syntax:* flags _string list..._ ++
*Default:* pass_all
Flags to pass to the rspamd server.
See https://rspamd.com/doc/architecture/protocol.html for details.
## MAIL FROM and From authorization (check.authorize_sender)
This check verifies that envelope and header sender addresses belong
to the authenticated user. Address ownership is established via table
that maps each user account to a email address it is allowed to use.
There are some special cases, see user_to_email description below.
```
check.authorize_sender {
prepare_email identity
user_to_email identity
check_header yes
unauth_action reject
no_match_action reject
malformed_action reject
err_action reject
auth_normalize precis_casefold_email
from_normalize precis_casefold_email
}
```
```
check {
authorize_sender { ... }
}
```
## Configuration directives
*Syntax:* user_to_email _table_ ++
*Default:* identity
Table to use for lookups. Result of the lookup should contain either the
domain name, the full email address or "*" string. If it is just domain - user
will be allowed to use any mailbox within a domain as a sender address.
If result contains "*" - user will be allowed to use any address.
*Syntax:* check_header _boolean_ ++
*Default:* yes
Whether to verify header sender in addition to envelope.
Either Sender or From field value should match the
authorization identity.
*Syntax:* unauth_action _action_ ++
*Default:* reject
What to do if the user is not authenticated at all.
*Syntax:* no_match_action _action_ ++
*Default:* reject
What to do if user is not allowed to use the sender address specified.
*Syntax:* malformed_action _action_ ++
*Default:* reject
What to do if From or Sender header fields contain malformed values.
*Syntax:* err_action _action_ ++
*Default:* reject
What to do if error happens during prepare_email or user_to_email lookup.
*Syntax:* auth_normalize _action_ ++
*Default:* precis_casefold_email
Normalization function to apply to authorization username before
further processing.
Available options:
- precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain
- precis_casefold PRECIS UsernameCaseMapped profile for the entire string
- precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain
- precis PRECIS UsernameCasePreserved profile for the entire string
- casefold Convert to lower case
- noop Nothing
*Syntax:* from_normalize _action_ ++
*Default:* precis_casefold_email
Normalization function to apply to email addresses before
further processing.
Available options are same as for auth_normalize.

View file

@ -1,130 +0,0 @@
maddy-imap(5) "maddy mail server" "maddy reference documentation"
; TITLE IMAP endpoint module
Module 'imap' is a listener that implements IMAP4rev1 protocol and provides
access to local messages storage specified by 'storage' directive. See
*maddy-storage*(5) for support storage backends and corresponding
configuration options.
```
imap tcp://0.0.0.0:143 tls://0.0.0.0:993 {
tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
io_debug no
debug no
insecure_auth no
auth pam
storage &local_mailboxes
}
```
## Configuration directives
*Syntax*: tls _certificate_path_ _key_path_ { ... } ++
*Default*: global directive value
TLS certificate & key to use. Fine-tuning of other TLS properties is possible
by specifing a configuration block and options inside it:
```
tls cert.crt key.key {
protocols tls1.2 tls1.3
}
```
See section 'TLS configuration' in *maddy*(1) for valid options.
*Syntax*: io_debug _boolean_ ++
*Default*: no
Write all commands and responses to stderr.
*Syntax*: io_errors _boolean_ ++
*Default*: no
Log I/O errors.
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Enable verbose logging.
*Syntax*: insecure_auth _boolean_ ++
*Default*: no (yes if TLS is disabled)
*Syntax*: auth _module_reference_
Use the specified module for authentication.
*Required.*
*Syntax*: storage _module_reference_
Use the specified module for message storage.
*Required.*
## IMAP filters
Most storage backends support application of custom code late in delivery
process. As opposed to using SMTP pipeline modifiers or checks, it allows
modifying IMAP-specific message attributes. In particular, it allows
code to change target folder and add IMAP flags (keywords) to the message.
There is no way to reject message using IMAP filters, this should be done
eariler in SMTP pipeline logic. Quarantined messages are not processed
by IMAP filters and are unconditionally delivered to Junk folder (or other
folder with \Junk special-use attribute).
To use an IMAP filter, specify it in the 'imap_filter' directive for the
used storage backend, like this:
```
storage.imapsql local_mailboxes {
...
imap_filter {
command /etc/maddy/sieve.sh {account_name}
}
}
```
## System command filter (imap.filter.command)
This filter is similar to check.command module described in *maddy-filters*(5)
and runs system command
Usage:
```
command executable_name args... { }
```
Same as check.command, following placeholders are supported for command
arguments: {source_ip}, {source_host}, {source_rdns}, {msg_id}, {auth_user},
{sender}. Note: placeholders in command name are not processed to avoid
possible command injection attacks.
Additionally, for imap.filter.command, {account_name} placeholder is replaced
with effective IMAP account name.
Note that if you use provided systemd units on Linux, maddy executable is
sandboxed - all commands will be executed with heavily restricted filesystem
acccess and other privileges. Notably, /tmp is isolated and all directories
except for /var/lib/maddy and /run/maddy are read-only. You will need to modify
systemd unit if your command needs more privileges.
Command output should consist of zero or more lines. First one, if non-empty, overrides
destination folder. All other lines contain additional IMAP flags to add
to the message. If command wants to add flags without changing folder - first
line should be empty.
It is valid for command to not write anything to stdout. In this case its
execution will have no effect on delivery.
Output example:
```
Junk
```
In this case, message will be placed in the Junk folder.
```
$Label1
```
In this case, message will be placed in inbox and will have
'$Label1' added.

View file

@ -1,642 +0,0 @@
maddy-smtp(5) "maddy mail server" "maddy reference documentation"
; TITLE SMTP endpoint module
# SMTP endpoint module (smtp)
Module 'smtp' is a listener that implements ESMTP protocol with optional
authentication, LMTP and Submission support. Incoming messages are processed in
accordance with pipeline rules (explained in Message pipeline section below).
```
smtp tcp://0.0.0.0:25 {
hostname example.org
tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
io_debug no
debug no
insecure_auth no
read_timeout 10m
write_timeout 1m
max_message_size 32M
max_header_size 1M
auth pam
defer_sender_reject yes
dmarc yes
smtp_max_line_length 4000
limits {
endpoint rate 10
endpoint concurrency 500
}
# Example pipeline ocnfiguration.
destination example.org {
deliver_to &local_mailboxes
}
default_destination {
reject
}
}
```
## Configuration directives
*Syntax*: hostname _string_ ++
*Default*: global directive value
Server name to use in SMTP banner.
```
220 example.org ESMTP Service Ready
```
*Syntax*: tls _certificate_path_ _key_path_ { ... } ++
*Default*: global directive value
TLS certificate & key to use. Fine-tuning of other TLS properties is possible
by specifing a configuration block and options inside it:
```
tls cert.crt key.key {
protocols tls1.2 tls1.3
}
```
See section 'TLS configuration' in *maddy*(1) for valid options.
*Syntax*: io_debug _boolean_ ++
*Default*: no
Write all commands and responses to stderr.
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Enable verbose logging.
*Syntax*: insecure_auth _boolean_ ++
*Default*: no (yes if TLS is disabled)
Allow plain-text authentication over unencrypted connections. Not recommended!
*Syntax*: read_timeout _duration_ ++
*Default*: 10m
I/O read timeout.
*Syntax*: write_timeout _duration_ ++
*Default*: 1m
I/O write timeout.
*Syntax*: max_message_size _size_ ++
*Default*: 32M
Limit the size of incoming messages to 'size'.
*Syntax*: max_header_size _size_ ++
*Default*: 1M
Limit the size of incoming message headers to 'size'.
*Syntax*: auth _module_reference_ ++
*Default*: not specified
Use the specified module for authentication.
*Syntax*: defer_sender_reject _boolean_ ++
*Default*: yes
Apply sender-based checks and routing logic when first RCPT TO command
is received. This allows maddy to log recipient address of the rejected
message and also improves interoperability with (improperly implemented)
clients that don't expect an error early in session.
*Syntax*: max_logged_rcpt_errors _integer_ ++
*Default*: 5
Amount of RCPT-time errors that should be logged. Further errors will be
handled silently. This is to prevent log flooding during email dictonary
attacks (address probing).
*Syntax*: max_received _integer_ ++
*Default*: 50
Max. amount of Received header fields in the message header. If the incoming
message has more fields than this number, it will be rejected with the permanent error
5.4.6 ("Routing loop detected").
*Syntax*: ++
buffer ram ++
buffer fs _[path]_ ++
buffer auto _max_size_ _[path]_ ++
*Default*: auto 1M StateDirectory/buffer
Temporary storage to use for the body of accepted messages.
- ram
Store the body in RAM.
- fs
Write out the message to the FS and read it back as needed.
_path_ can be omitted and defaults to StateDirectory/buffer.
- auto
Store message bodies smaller than _max_size_ entirely in RAM, otherwise write
them out to the FS.
_path_ can be omitted and defaults to StateDirectory/buffer.
*Syntax*: smtp_max_line_length _integer_ ++
*Default*: 4000
The maximum line length allowed in the SMTP input stream. If client sends a
longer line - connection will be closed and message (if any) will be rejected
with a permanent error.
RFC 5321 has the recommended limit of 998 bytes. Servers are not required
to handle longer lines correctly but some senders may produce them.
Unless BDAT extension is used by the sender, this limitation also applies to
the message body.
*Syntax*: dmarc _boolean_ ++
*Default*: yes
Enforce sender's DMARC policy. Due to implementation limitations, it is not a
check module.
*NOTE*: Report generation is not implemented now.
*NOTE*: DMARC needs SPF and DKIM checks to function correctly.
Without these, DMARC check will not run.
## Rate & concurrency limiting
*Syntax*: limits _config block_ ++
*Default*: no limits
This allows configuring a set of message flow restrictions including
max. concurrency and rate per-endpoint, per-source, per-destination.
Limits are specified as directives inside the block:
```
limits {
all rate 20
destination concurrency 5
}
```
Supported limits:
- Rate limit
*Syntax*: _scope_ rate _burst_ _[period]_ ++
Restrict the amount of messages processed in _period_ to _burst_ messages.
If period is not specified, 1 second is used.
- Concurrency limit
*Syntax*: _scope_ concurrency _max_ ++
Restrict the amount of messages processed in parallel to _max_.
For each supported limitation, _scope_ determines whether it should be applied
for all messages ("all"), per-sender IP ("ip"), per-sender domain ("source") or
per-recipient domain ("destination"). Having a scope other than "all" means
that the restriction will be enforced independently for each group determined
by scope. E.g. "ip rate 20" means that the same IP cannot send more than 20
messages in a scond. "destination concurrency 5" means that no more than 5
messages can be sent in parallel to a single domain.
*Note*: At the moment, SMTP endpoint on its own does not support per-recipient
limits. They will be no-op. If you want to enforce a per-recipient restriction
on outbound messages, do so using 'limits' directive for the 'remote' module
(see *maddy-targets*(5)).
It is possible to share limit counters between multiple endpoints (or any other
modules). To do so define a top-level configuration block for module "limits"
and reference it where needed using standard & syntax. E.g.
```
limits inbound_limits {
all rate 20
}
smtp smtp://0.0.0.0:25 {
limits &inbound_limits
...
}
submission tls://0.0.0.0:465 {
limits &inbound_limits
...
}
```
Using an "all rate" restriction in such way means that no more than 20
messages can enter the server through both endpoints in one second.
# Submission module (submission)
Module 'submission' implements all functionality of the 'smtp' module and adds
certain message preprocessing on top of it, additionaly authentication is
always required.
'submission' module checks whether addresses in header fields From, Sender, To,
Cc, Bcc, Reply-To are correct and adds Message-ID and Date if it is missing.
```
submission tcp://0.0.0.0:587 tls://0.0.0.0:465 {
# ... same as smtp ...
}
```
# LMTP module (lmtp)
Module 'lmtp' implements all functionality of the 'smtp' module but uses
LMTP (RFC 2033) protocol.
```
lmtp unix://lmtp.sock {
# ... same as smtp ...
}
```
## Limitations of LMTP implementation
- Can't be used with TCP.
- Per-recipient status is not supported.
- Delivery to 'sql' module storage is always atomic, either all recipients will
succeed or none of them will.
# Mesage pipeline
Message pipeline is a set of module references and associated rules that
describe how to handle messages.
The pipeline is responsible for
- Running message filters (called "checks"), (e.g. DKIM signature verification,
DNSBL lookup and so on).
- Running message modifiers (e.g. DKIM signature creation).
- Assocating each message recipient with one or more delivery targets.
Delivery target is a module that does final processing (delivery) of the
message.
Message handling flow is as follows:
- Execute checks referenced in top-level 'check' blocks (if any)
- Execute modifiers referenced in top-level 'modify' blocks (if any)
- If there are 'source' blocks - select one that matches message sender (as
specified in MAIL FROM). If there are no 'source' blocks - entire
configuration is assumed to be the 'default_source' block.
- Execute checks referenced in 'check' blocks inside selected 'source' block
(if any).
- Execute modifiers referenced in 'modify' blocks inside selected 'source'
block (if any).
Then, for each recipient:
- Select 'destination' block that matches it. If there are
no 'destination' blocks - entire used 'source' block is interpreted as if it
was a 'default_destination' block.
- Execute checks referenced in 'check' block inside selected 'destination' block
(if any).
- Execute modifiers referenced in 'modify' block inside selected 'destination'
block (if any).
- If used block contains 'reject' directive - reject the recipient with
specified SMTP status code.
- If used block contains 'deliver_to' directive - pass the message to the
specified target module. Only recipients that are handled
by used block are visible to the target.
Each recipient is handled only by a single 'destination' block, in case of
overlapping 'destination' - first one takes priority.
```
destination example.org {
deliver_to targetA
}
destination example.org { # ambiguous and thus not allowed
deliver_to targetB
}
```
Same goes for 'source' blocks, each message is handled only by a single block.
Each recipient block should contain at least one 'deliver_to' directive or
'reject' directive. If 'destination' blocks are used, then
'default_destination' block should also be used to specify behavior for
unmatched recipients. Same goes for source blocks, 'default_source' should be
used if 'source' is used.
That is, pipeline configuration should explicitly specify behavior for each
possible sender/recipient combination.
Additionally, directives that specify final handling decision ('deliver_to',
'reject') can't be used at the same level as source/destination rules.
Consider example:
```
destination example.org {
deliver_to local_mboxes
}
reject
```
It is not obvious whether 'reject' applies to all recipients or
just for non-example.org ones, hence this is not allowed.
Complete configuration example using all of the mentioned directives:
```
check {
# Run a check to make sure source SMTP server identification
# is legit.
require_matching_ehlo
}
# Messages coming from senders at example.org will be handled in
# accordance with the following configuration block.
source example.org {
# We are example.com, so deliver all messages with recipients
# at example.com to our local mailboxes.
destination example.com {
deliver_to &local_mailboxes
}
# We don't do anything with recipients at different domains
# because we are not an open relay, thus we reject them.
default_destination {
reject 521 5.0.0 "User not local"
}
}
# We do our business only with example.org, so reject all
# other senders.
default_source {
reject
}
```
## Directives
*Syntax*: check _block name_ { ... } ++
*Context*: pipeline configuration, source block, destination block
List of the module references for checks that should be executed on
messages handled by block where 'check' is placed in.
Note that message body checks placed in destination block are currently
ignored. Due to the way SMTP protocol is defined, they would cause message to
be rejected for all recipients which is not what you usually want when using
such configurations.
Example:
```
check {
# Reference implicitly defined default configuration for check.
require_matching_ehlo
# Inline definition of custom config.
require_source_mx {
# Configuration for require_source_mx goes here.
fail_action reject
}
}
```
It is also possible to define the block of checks at the top level
as "checks" module and reference it using & syntax. Example:
```
checks inbound_checks {
require_matching_ehlo
}
# ... somewhere else ...
{
...
check &inbound_checks
}
```
*Syntax*: modify { ... } ++
*Default*: not specified ++
*Context*: pipeline configuration, source block, destination block
List of the module references for modifiers that should be executed on
messages handled by block where 'modify' is placed in.
Message modifiers are similar to checks with the difference in that checks
purpose is to verify whether the message is legitimate and valid per local
policy, while modifier purpose is to post-process message and its metadata
before final delivery.
For example, modifier can replace recipient address to make message delivered
to the different mailbox or it can cryptographically sign outgoing message
(e.g. using DKIM). Some modifier can perform multiple unrelated modifications
on the message.
*Note*: Modifiers that affect source address can be used only globally or on
per-source basis, they will be no-op inside destination blocks. Modifiers that
affect the message header will affect it for all recipients.
It is also possible to define the block of modifiers at the top level
as "modiifers" module and reference it using & syntax. Example:
```
modifiers local_modifiers {
replace_rcpt file /etc/maddy/aliases
}
# ... somewhere else ...
{
...
modify &local_modifiers
}
```
*Syntax*: ++
reject _smtp_code_ _smtp_enhanced_code_ _error_description_ ++
reject _smtp_code_ _smtp_enhanced_code_ ++
reject _smtp_code_ ++
reject ++
*Context*: destination block
Messages handled by the configuration block with this directive will be
rejected with the specified SMTP error.
If you aren't sure which codes to use, use 541 and 5.4.0 with your message or
just leave all arguments out, the error description will say "message is
rejected due to policy reasons" which is usually what you want to mean.
'reject' can't be used in the same block with 'deliver_to' or
'destination/source' directives.
Example:
```
reject 541 5.4.0 "We don't like example.org, go away"
```
*Syntax*: deliver_to _target-config-block_ ++
*Context*: pipeline configuration, source block, destination block
Deliver the message to the referenced delivery target. What happens next is
defined solely by used target. If deliver_to is used inside 'destination'
block, only matching recipients will be passed to the target.
*Syntax*: source_in _table reference_ { ... } ++
*Context*: pipeline configuration
Handle messages with envelope senders present in the specified table in
accordance with the specified configuration block.
Takes precedence over all 'sender' directives.
Example:
```
source_in file /etc/maddy/banned_addrs {
reject 550 5.7.0 "You are not welcome here"
}
source example.org {
...
}
...
```
See 'destination_in' documentation for note about table configuration.
*Syntax*: source _rules..._ { ... } ++
*Context*: pipeline configuration
Handle messages with MAIL FROM value (sender address) matching any of the rules
in accordance with the specified configuration block.
"Rule" is either a domain or a complete address. In case of overlapping
'rules', first one takes priority. Matching is case-insensitive.
Example:
```
# All messages coming from example.org domain will be delivered
# to local_mailboxes.
source example.org {
deliver_to &local_mailboxes
}
# Messages coming from different domains will be rejected.
default_source {
reject 521 5.0.0 "You were not invited"
}
```
*Syntax*: reroute { ... } ++
*Context*: pipeline configuration, source block, destination block
This directive allows to make message routing decisions based on the
result of modifiers. The block can contain all pipeline directives and they
will be handled the same with the exception that source and destination rules
will use the final recipient and sender values (e.g. after all modifiers are
applied).
Here is the concrete example how it can be useful:
```
destination example.org {
modify {
replace_rcpt file /etc/maddy/aliases
}
reroute {
destination example.org {
deliver_to &local_mailboxes
}
default_destination {
deliver_to &remote_queue
}
}
}
```
This configuration allows to specify alias local addresses to remote ones
without being an open relay, since remote_queue can be used only if remote
address was introduced as a result of rewrite of local address.
*WARNING*: If you have DMARC enabled (default), results generated by SPF
and DKIM checks inside a reroute block *will not* be considered in DMARC
evaluation.
*Syntax*: destination_in _table reference_ { ... } ++
*Context*: pipeline configuration, source block
Handle messages with envelope recipients present in the specified table in
accordance with the specified configuration block.
Takes precedence over all 'destination' directives.
Example:
```
destination_in file /etc/maddy/remote_addrs {
deliver_to smtp tcp://10.0.0.7:25
}
destination example.com {
deliver_to &local_mailboxes
}
...
```
Note that due to the syntax restrictions, it is not possible to specify
extended configuration for table module. E.g. this is not valid:
```
destination_in sql_table {
dsn ...
driver ...
} {
deliver_to whatever
}
```
In this case, configuration should be specified separately and be referneced
using '&' syntax:
```
table.sql_table remote_addrs {
dsn ...
driver ...
}
whatever {
destination_in &remote_addrs {
deliver_to whatever
}
}
```
*Syntax*: destination _rule..._ { ... } ++
*Context*: pipeline configuration, source block
Handle messages with RCPT TO value (recipient address) matching any of the
rules in accordance with the specified configuration block.
"Rule" is either a domain or a complete address. Duplicate rules are not
allowed. Matching is case-insensitive.
Note that messages with multiple recipients are split into multiple messages if
they have recipients matched by multiple blocks. Each block will see the
message only with recipients matched by its rules.
Example:
```
# Messages with recipients at example.com domain will be
# delivered to local_mailboxes target.
destination example.com {
deliver_to &local_mailboxes
}
# Messages with other recipients will be rejected.
default_destination {
rejected 541 5.0.0 "User not local"
}
```
## Reusable pipeline parts (msgpipeline module)
The message pipeline can be used independently of the SMTP module in other
contexts that require a delivery target.
Full pipeline functionality can be used where a delivery target is expected.

View file

@ -1,201 +0,0 @@
maddy-targets(5) "maddy mail server" "maddy reference documentation"
; TITLE Storage backends
maddy storage interface is built with IMAP in mind and directly represents
IMAP data model. That is, maddy storage does have the concept of folders,
flags, message UIDs, etc defined as in RFC 3501.
This man page lists supported storage backends along with supported
configuration directives for each.
Most likely, you are going to use modules listed here in 'storage' directive
for IMAP endpoint module (see *maddy-imap*(5)).
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.
# SQL-based database module (storage.imapsql)
The imapsql module implements database for IMAP index and message
metadata using SQL-based relational database.
Message contents are stored in an "external store" defined by msg_store
directive. By default this is a file system directory under /var/lib/maddy.
Supported RDBMS:
- SQLite 3.25.0
- PostgreSQL 9.6 or newer
Account names are required to have the form of a email address and are
case-insensitive. UTF-8 names are supported with restrictions defined in the
PRECIS UsernameCaseMapped profile.
```
storage.imapsql {
driver sqlite3
dsn imapsql.db
msg_store fs messages/
}
```
imapsql module also can be used as a lookup table (*maddy-table*(5)).
It returns empty string values for existing usernames. This might be useful
with destination_in directive (*maddy-smtp*(5)) e.g. to implement catch-all
addresses (this is a bad idea to do so, this is just an example):
```
destination_in &local_mailboxes {
deliver_to &local_mailboxes
}
destination example.org {
modify {
replace_rcpt regexp ".*" "catchall@example.org"
}
deliver_to &local_mailboxes
}
```
## Arguments
Specify the driver and DSN.
## Configuration directives
*Syntax*: driver _string_ ++
*Default*: not specified
REQUIRED.
Use a specified driver to communicate with the database. Supported values:
sqlite3, postgres.
Should be specified either via an argument or via this directive.
*Syntax*: dsn _string_ ++
*Default*: not specified
REQUIRED.
Data Source Name, the driver-specific value that specifies the database to use.
For SQLite3 this is just a file path.
For PostgreSQL: https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
Should be specified either via an argument or via this directive.
*Syntax*: msg_store _store_ ++
*Default*: fs messages/
Module to use for message bodies storage.
See *maddy-blob*(5) for details.
*Syntax*: ++
compression off ++
compression _algorithm_ ++
compression _algorithm_ _level_ ++
*Default*: off
Apply compression to message contents.
Supported algorithms: lz4, zstd.
*Syntax*: appendlimit _size_ ++
*Default*: 32M
Don't allow users to add new messages larger than 'size'.
This does not affect messages added when using module as a delivery target.
Use 'max_message_size' directive in SMTP endpoint module to restrict it too.
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Enable verbose logging.
*Syntax*: junk_mailbox _name_ ++
*Default*: Junk
The folder to put quarantined messages in. Thishis setting is not used if user
does have a folder with "Junk" special-use attribute.
*Syntax*: sqlite_exclusive_lock _boolean_ ++
*Default*: no
SQLite-specific performance tuning option. Slightly decereases ovehead of
DB locking at cost of making DB inaccessible for other processes (including
maddyctl utility).
*Syntax*: sqlite_cache_size _integer_ ++
*Default*: defined by SQLite
SQLite page cache size. If positive - specifies amount of pages (1 page - 4
KiB) to keep in cache. If negative - specifies approximate upper bound
of cache size in KiB.
*Syntax*: sqlite_busy_timeout _integer_ ++
*Default*: 5000000
SQLite-specific performance tuning option. Amount of milliseconds to wait
before giving up on DB lock.
*Syntax*: imap_filter { ... } ++
*Default*: not set
Specifies IMAP filters to apply for messages delivered from SMTP pipeline.
See *maddy-imap*(5) for filter modules usable here.
Ex.
```
imap_filter {
command /etc/maddy/sieve.sh {account_name}
}
```
*Syntax:* delivery_map *table* ++
*Default:* identity
Use specified table module (*maddy-tables*(5)) to map recipient
addresses from incoming messages to mailbox names.
Normalization algorithm specified in delivery_normalize is appied before
delivery_map.
*Syntax:* delivery_normalize _name_ ++
*Default:* precis_casefold_email
Normalization function to apply to email addresses before mapping them
to mailboxes.
See auth_normalize.
*Syntax*: auth_map *table* ++
*Default*: identity
Use specified table module (*maddy-tables*(5)) to map authentication
usernames to mailbox names.
Normalization algorithm specified in auth_normalize is applied before
auth_map.
*Syntax*: auth_normalize _name_ ++
*Default*: precis_casefold_email
Normalization function to apply to authentication usernames before mapping
them to mailboxes.
Available options:
- precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain
- precis_casefold PRECIS UsernameCaseMapped profile for the entire string
- precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain
- precis PRECIS UsernameCasePreserved profile for the entire string
- casefold Convert to lower case
- noop Nothing
Note: On message delivery, recipient address is unconditionally normalized
using precis_casefold_email function.

View file

@ -1,314 +0,0 @@
maddy-tables(5) "maddy mail server" "maddy reference documentation"
; TITLE String-string translation
Whenever you need to replace one string with another when handling anything in
maddy, you can use any of the following modules to obtain the replacement
string. They are commonly called "table modules" or just "tables".
Some table modules implement write options allowing other maddy modules to
change the source of data, effectively turning the table into a complete
interface to a key-value store for maddy. Such tables are referred to as
"mutable tables".
# File mapping (table.file)
This module builds string-string mapping from a text file.
File is reloaded every 15 seconds if there are any changes (detected using
modification time). No changes are applied if file contains syntax errors.
Definition:
```
file <file path>
```
or
```
file {
file <file path>
}
```
Usage example:
```
# Resolve SMTP address aliases using text file mapping.
modify {
replace_rcpt file /etc/maddy/aliases
}
```
## Syntax
Better demonstrated by examples:
```
# Lines starting with # are ignored.
# And so are lines only with whitespace.
# Whenever 'aaa' is looked up, return 'bbb'
aaa: bbb
# Trailing and leading whitespace is ignored.
ccc: ddd
# If there is no colon, the string is translated into ""
# That is, the following line is equivalent to
# aaa:
aaa
# If the same key is used multiple times - table.file will return
# multiple values when queries. Note that this is not used by
# most modules. E.g. replace_rcpt does not (intentionally) support
# 1-to-N alias expansion.
ddd: firstvalue
ddd: secondvalue
```
# SQL query mapping (table.sql_query)
The sql_query module implements table interface using SQL queries.
Definition:
```
table.sql_query {
driver <driver name>
dsn <data source name>
lookup <lookup query>
# Optional:
init <init query list>
list <list query>
add <add query>
del <del query>
set <set query>
}
```
Usage example:
```
# Resolve SMTP address aliases using PostgreSQL DB.
modify {
replace_rcpt sql_query {
driver postgres
dsn "dbname=maddy user=maddy"
lookup "SELECT alias FROM aliases WHERE address = $1"
}
}
```
## Configuration directives
**Syntax**: driver _driver name_ ++
**REQUIRED**
Driver to use to access the database.
Supported drivers: postgres, sqlite3 (if compiled with C support)
**Syntax**: dsn _data source name_ ++
**REQUIRED**
Data Source Name to pass to the driver. For SQLite3 this is just a path to DB
file. For Postgres, see
https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection_String_Parameters
**Syntax**: lookup _query_ ++
**REQUIRED**
SQL query to use to obtain the lookup result.
It will get one named argument containing the lookup key. Use :key
placeholder to access it in SQL. The result row set should contain one row, one
column with the string that will be used as a lookup result. If there are more
rows, they will be ignored. If there are more columns, lookup will fail. If
there are no rows, lookup returns "no results". If there are any error - lookup
will fail.
**Syntax**: init _queries..._ ++
**Default**: empty
List of queries to execute on initialization. Can be used to configure RDBMS.
Example, to improve SQLite3 performance:
```
table.sql_query {
driver sqlite3
dsn whatever.db
init "PRAGMA journal_mode=WAL" \
"PRAGMA synchronous=NORMAL"
lookup "SELECT alias FROM aliases WHERE address = $1"
}
```
*Syntax:* named_args _boolean_ ++
*Default:* yes
Whether to use named parameters binding when executing SQL queries
or not.
Note that maddy's PostgreSQL driver does not support named parameters and
SQLite3 driver has issues handling numbered parameters:
https://github.com/mattn/go-sqlite3/issues/472
**Syntax:** add _query_ ++
**Syntax:** list _query_ ++
**Syntax:** set _query_ ++
**Syntax:** del _query_ ++
**Default:** none
If queries are set to implement corresponding table operations - table becomes
"mutable" and can be used in contexts that require writable key-value store.
'add' query gets :key, :value named arguments - key and value strings to store.
They should be added to the store. The query *should* not add multiple values
for the same key and *should* fail if the key already exists.
'list' query gets no arguments and should return a column with all keys in
the store.
'set' query gets :key, :value named arguments - key and value and should replace the existing
entry in the database.
'del' query gets :key argument - key and should remove it from the database.
If named_args is set to "no" - key is passed as the first numbered parameter
($1), value is passed as the second numbered parameter ($2).
# Static table (table.static)
The 'static' module implements table lookups using key-value pairs in its
configuration.
```
table.static {
entry KEY1 VALUE1
entry KEY2 VALUE2
...
}
```
## Configuration directives
**Syntax**: entry _key_ _value_
Add an entry to the table.
If the same key is used multiple times, the last one takes effect.
# Regexp rewrite table (table.regexp)
The 'regexp' module implements table lookups by applying a regular expression
to the key value. If it matches - 'replacement' value is returned with $N
placeholders being replaced with corresponding capture groups from the match.
Otherwise, no value is returned.
The regular expression syntax is the subset of PCRE. See
https://golang.org/pkg/regexp/syntax/ for details.
```
table.regexp <regexp> [replacement] {
full_match yes
case_insensitive yes
expand_placeholders yes
}
```
Note that [replacement] is optional. If it is not included - table.regexp
will return the original string, therefore acting as a regexp match check.
This can be useful in combination in destination_in (*maddy-smtp*(5)) for
advanced matching:
```
destination_in regexp ".*-bounce+.*@example.com" {
...
}
```
## Configuration directives
**Syntax**: full_match _boolean_ ++
**Default**: yes
Whether to implicitly add start/end anchors to the regular expression.
That is, if 'full_match' is yes, then the provided regular expression should
match the whole string. With no - partial match is enough.
**Syntax**: case_insensitive _boolean_ ++
**Default**: yes
Whether to make matching case-insensitive.
**Syntax**: expand_placeholders _boolean_ ++
**Default**: yes
Replace '$name' and '${name}' in the replacement string with contents of
corresponding capture groups from the match.
To insert a literal $ in the output, use $$ in the template.
# Identity table (table.identity)
The module 'identity' is a table module that just returns the key looked up.
```
table.identity { }
```
# No-op table (dummy)
The module 'dummy' represents an empty table.
```
dummy { }
```
# Email local part (table.email_localpart)
The module 'email_localpart' extracts and unescaped local ("username") part
of the email address.
E.g.
test@example.org => test
"test @ a"@example.org => test @ a
```
table.email_localpart { }
```
# Table chaining module (table.chain)
The table.chain module allows chaining together multiple table modules
by using value returned by a previous table as an input for the second
table.
Example:
```
table.chain {
step regexp "(.+)(\\+[^+"@]+)?@example.org" "$1@example.org"
step file /etc/maddy/emails
}
```
This will strip +prefix from mailbox before looking it up
in /etc/maddy/emails list.
## Configuration directives
*Syntax*: step _table_
Adds a table module to the chain. If input value is not in the table
(e.g. file) - return "not exists" error.
*Syntax*: optional_step _table_
Same as step but if input value is not in the table - it is passed to the
next step without changes.
Example:
Something like this can be used to map emails to usernames
after translating them via aliases map:
```
table.chain {
optional_step file /etc/maddy/aliases
step regexp "(.+)@(.+)" "$1"
}
```

View file

@ -1,468 +0,0 @@
maddy-targets(5) "maddy mail server" "maddy reference documentation"
; TITLE Delivery targets
This man page describes modules that can used with 'deliver_to' directive
of SMTP endpoint module.
# SQL module (target.imapsql)
SQL module described in *maddy-storage*(5) can also be used as a delivery
target.
# Queue module (target.queue)
Queue module buffers messages on disk and retries delivery multiple times to
another target to ensure reliable delivery.
```
target.queue {
target remote
location ...
max_parallelism 16
max_tries 4
bounce {
destination example.org {
deliver_to &local_mailboxes
}
default_destination {
reject
}
}
autogenerated_msg_domain example.org
debug no
}
```
## Arguments
First argument specifies directory to use for storage.
Relative paths are relative to the StateDirectory.
## Configuration directives
*Syntax*: target _block_name_ ++
*Default*: not specified
REQUIRED.
Delivery target to use for final delivery.
*Syntax*: location _directory_ ++
*Default*: StateDirectory/configuration_block_name
File system directory to use to store queued messages.
Relative paths are relative to the StateDirectory.
*Syntax*: max_parallelism _integer_ ++
*Default*: 16
Start up to _integer_ goroutines for message processing. Basically, this option
limits amount of messages tried to be delivered concurrently.
*Syntax*: max_tries _integer_ ++
*Default*: 20
Attempt delivery up to _integer_ times. Note that no more attempts will be done
is permanent error occured during previous attempt.
Delay before the next attempt will be increased exponentally using the
following formula: 15mins \* 1.2 ^ (n - 1) where n is the attempt number.
This gives you approximately the following sequence of delays:
18mins, 21mins, 25mins, 31mins, 37mins, 44mins, 53mins, 64mins, ...
*Syntax*: bounce { ... } ++
*Default*: not specified
This configuration contains pipeline configuration to be used for generated DSN
(Delivery Status Notifiaction) messages.
If this is block is not present in configuration, DSNs will not be generated.
Note, however, this is not what you want most of the time.
*Syntax*: autogenerated_msg_domain _domain_ ++
*Default*: global directive value
Domain to use in sender address for DSNs. Should be specified too if 'bounce'
block is specified.
*Syntax*: debug _boolean_ ++
*Default*: no
Enable verbose logging.
# Remote MX module (remote)
Module that implements message delivery to remote MTAs discovered via DNS MX
records. You probably want to use it with queue module for reliability.
```
target.remote {
hostname mx.example.org
debug no
}
```
If a message check marks a message as 'quarantined', remote module
will refuse to deliver it.
## Configuration directives
*Syntax*: hostname _domain_ ++
*Default*: global directive value
Hostname to use client greeting (EHLO/HELO command). Some servers require it to
be FQDN, SPF-capable servers check whether it corresponds to the server IP
address, so it is better to set it to a domain that resolves to the server IP.
*Syntax*: limits _config block_ ++
*Default*: no limits
See 'limits' directive in *maddy-smtp*(5) for SMTP endpoint.
It works the same except for address domains used for
per-source/per-destination are as observed when message exits the server.
*Syntax*: local_ip _IP address_ ++
*Default*: empty
Choose the local IP to bind for outbound SMTP connections.
*Syntax*: force_ipv4 _boolean_ ++
*Default*: false
Force resolving outbound SMTP domains to IPv4 addresses. Some server providers
do not offer a way to properly set reverse PTR domains for IPv6 addresses; this
option makes maddy only connect to IPv4 addresses so that its public IPv4 address
is used to connect to that server, and thus reverse PTR checks are made against
its IPv4 address.
Warning: this may break sending outgoing mail to IPv6-only SMTP servers.
*Syntax*: connect_timeout _duration_ ++
*Default*: 5m
Timeout for TCP connection establishment.
RFC 5321 recommends 5 minutes for "initial greeting" that includes TCP
handshake. maddy uses two separate timers - one for "dialing" (DNS A/AAAA
lookup + TCP handshake) and another for "initial greeting". This directive
configures the former. The latter is not configurable and is hardcoded to be
5 minutes.
*Syntax*: command_timeout _duration_ ++
*Default*: 5m
Timeout for any SMTP command (EHLO, MAIL, RCPT, DATA, etc).
If STARTTLS is used this timeout also applies to TLS handshake.
RFC 5321 recommends 5 minutes for MAIL/RCPT and 3 minutes for
DATA.
*Syntax*: submission_timeout _duration_ ++
*Default*: 12m
Time to wait after the entire message is sent (after "final dot").
RFC 5321 recommends 10 minutes.
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Enable verbose logging.
*Syntax*: requiretls_override _boolean_ ++
*Default*: true
Allow local security policy to be disabled using 'TLS-Required' header field in
sent messages. Note that the field has no effect if transparent forwarding is
used, message body should be processed before outbound delivery starts for it
to take effect (e.g. message should be queued using 'queue' module).
*Syntax*: relaxed_requiretls _boolean_ ++
*Default*: true
This option disables strict conformance with REQUIRETLS specification and
allows forwarding of messages 'tagged' with REQUIRETLS to MXes that are not
advertising REQUIRETLS support. It is meant to allow REQUIRETLS use without the
need to have support from all servers. It is based on the assumption that
server referenced by MX record is likely the final destination and therefore
there is only need to secure communication towards it and not beyond.
*Syntax*: conn_reuse_limit _integer_ ++
*Default*: 10
Amount of times the same SMTP connection can be used.
Connections are never reused if the previous DATA command failed.
*Syntax*: conn_max_idle_count _integer_ ++
*Default*: 10
Max. amount of idle connections per recipient domains to keep in cache.
*Syntax*: conn_max_idle_time _integer_ ++
*Default*: 150 (2.5 min)
Amount of time the idle connection is still considered potentially usable.
## Security policies
*Syntax*: mx_auth _config block_ ++
*Default*: no policies
'remote' module implements a number of of schemes and protocols necessary to
ensure security of message delivery. Most of these schemes are concerned with
authentication of recipient server and TLS enforcement.
To enable mechanism, specify its name in the mx_auth directive block:
```
mx_auth {
dane
mtasts
}
```
Additional configuration is possible if supported by the mechanism by
specifying additional options as a block for the corresponding mechanism.
E.g.
```
mtasts {
cache ram
}
```
If the mx_auth directive is not specified, no mechanisms are enabled. Note
that, however, this makes outbound SMTP vulnerable to a numberous downgrade
attacks and hence not recommended.
It is possible to share the same set of policies for multiple 'remote' module
instances by defining it at the top-level using 'mx_auth' module and then
referencing it using standard & syntax:
```
mx_auth outbound_policy {
dane
mtasts {
cache ram
}
}
# ... somewhere else ...
deliver_to remote {
mx_auth &outbound_policy
}
# ... somewhere else ...
deliver_to remote {
mx_auth &outbound_policy
tls_client { ... }
}
```
## Security policies: MTA-STS
Checks MTA-STS policy of the recipient domain. Provides proper authentication
and TLS enforcement for delivery, but partially vulnerable to persistent active
attacks.
Sets MX level to "mtasts" if the used MX matches MTA-STS policy even if it is
not set to "enforce" mode.
```
mtasts {
cache fs
fs_dir StateDirectory/mtasts_cache
}
```
*Syntax*: cache fs|ram ++
*Default*: fs
Storage to use for MTA-STS cache. 'fs' is to use a filesystem directory, 'ram'
to store the cache in memory.
It is recommended to use 'fs' since that will not discard the cache (and thus
cause MTA-STS security to disappear) on server restart. However, using the RAM
cache can make sense for high-load configurations with good uptime.
*Syntax*: fs_dir _directory_ ++
*Default*: StateDirectory/mtasts_cache
Filesystem directory to use for policies caching if 'cache' is set to 'fs'.
## Security policies: DNSSEC
Checks whether MX records are signed. Sets MX level to "dnssec" is they are.
maddy does not validate DNSSEC signatures on its own. Instead it reslies on
the upstream resolver to do so by causing lookup to fail when verification
fails and setting the AD flag for signed and verfified zones. As a safety
measure, if the resolver is not 127.0.0.1 or ::1, the AD flag is ignored.
DNSSEC is currently not supported on Windows and other platforms that do not
have the /etc/resolv.conf file in the standard format.
```
dnssec { }
```
## Security policies: DANE
Checks TLSA records for the recipient MX. Provides downgrade-resistant TLS
enforcement.
Sets TLS level to "authenticated" if a valid and matching TLSA record uses
DANE-EE or DANE-TA usage type.
See above for notes on DNSSEC. DNSSEC support is required for DANE to work.
```
dane { }
```
## Security policies: Local policy
Checks effective TLS and MX levels (as set by other policies) against local
configuration.
```
local_policy {
min_tls_level none
min_mx_level none
}
```
Using 'local_policy off' is equivalent to setting both directives to 'none'.
*Syntax*: min_tls_level none|encrypted|authenticated ++
*Default*: none
Set the minimal TLS security level required for all outbound messages.
See [Security levels](../../seclevels) page for details.
*Syntax*: min_mx_level: none|mtasts|dnssec ++
*Default*: none
Set the minimal MX security level required for all outbound messages.
See [Security levels](../../seclevels) page for details.
# SMTP transparent forwarding module (target.smtp)
Module that implements transparent forwarding of messages over SMTP.
Use in pipeline configuration:
```
deliver_to smtp tcp://127.0.0.1:5353
# or
deliver_to smtp tcp://127.0.0.1:5353 {
# Other settings, see below.
}
```
```
target.smtp {
debug no
tls_client {
...
}
attempt_starttls yes
require_tls no
auth off
targets tcp://127.0.0.1:2525
connect_timeout 5m
command_timeout 5m
submission_timeout 12m
}
```
Endpoint addresses use format described in *maddy-config*(5).
## Configuration directives
*Syntax*: debug _boolean_ ++
*Default*: global directive value
Enable verbose logging.
*Syntax*: tls_client { ... } ++
*Default*: not specified
Advanced TLS client configuration options. See *maddy-tls*(5) for details.
*Syntax*: attempt_starttls _boolean_ ++
*Default*: yes (no for target.lmtp)
Attempt to use STARTTLS if it is supported by the remote server.
If TLS handshake fails, connection will be retried without STARTTLS
unless 'require_tls' is also specified.
*Syntax*: require_tls _boolean_ ++
*Default*: no
Refuse to pass messages over plain-text connections.
*Syntax*: ++
auth off ++
plain _username_ _password_ ++
forward ++
external ++
*Default*: off
Specify the way to authenticate to the remote server.
Valid values:
- off
No authentication.
- plain
Authenticate using specified username-password pair.
*Don't use* this without enforced TLS ('require_tls').
- forward
Forward credentials specified by the client.
*Don't use* this without enforced TLS ('require_tls').
- external
Request "external" SASL authentication. This is usually used for
authentication using TLS client certificates. See *maddy-tls*(5)
for how to specify the client certificate.
*Syntax*: targets _endpoints..._ ++
*Default:* not specified
REQUIRED.
List of remote server addresses to use. See Address definitions in
*maddy-config*(5) for syntax to use. Basically, it is 'tcp://ADDRESS:PORT'
for plain SMTP and 'tls://ADDRESS:PORT' for SMTPS (aka SMTP with Implicit
TLS).
Multiple addresses can be specified, they will be tried in order until connection to
one succeeds (including TLS handshake if TLS is required).
*Syntax*: connect_timeout _duration_ ++
*Default*: 5m
Same as for target.remote.
*Syntax*: command_timeout _duration_ ++
*Default*: 5m
Same as for target.remote.
*Syntax*: submission_timeout _duration_ ++
*Default*: 12m
Same as for target.remote.
# LMTP transparent forwarding module (target.lmtp)
The 'target.lmtp' module is similar to 'target.smtp' and supports all
its options and syntax but speaks LMTP instead of SMTP.

View file

@ -1,379 +0,0 @@
maddy-tls(5) "maddy mail server" "maddy reference documentation"
; TITLE Advanced TLS configuration
# TLS server configuration
TLS certificates are obtained by modules called "certificate loaders". 'tls' directive
arguments specify name of loader to use and arguments. Due to syntax limitations
advanced configuration for loader should be specified using 'loader' directive, see
below.
```
tls file cert.pem key.pem {
protocols tls1.2 tls1.3
curve X25519
ciphers ...
}
tls {
loader file cert.pem key.pem {
# Options for loader go here.
}
protocols tls1.2 tls1.3
curve X25519
ciphers ...
}
```
## Available certificate loaders
- file
Accepts argument pairs specifying certificate and then key.
E.g. 'tls file certA.pem keyA.pem certB.pem keyB.pem'
If multiple certificates are listed, SNI will be used.
- acme
Automatically obtains a certificate using ACME protocol (Let's Encrypt)
See below for details.
- off
Not really a loader but a special value for tls directive, explicitly disables TLS for
endpoint(s).
## Advanced TLS configuration
*Note: maddy uses secure defaults and TLS handshake is resistant to active downgrade attacks.*
*There is no need to change anything in most cases.*
*Syntax*: ++
protocols _min_version_ _max_version_ ++
protocols _version_ ++
*Default*: tls1.0 tls1.3
Minimum/maximum accepted TLS version. If only one value is specified, it will
be the only one usable version.
Valid values are: tls1.0, tls1.1, tls1.2, tls1.3
*Syntax*: ciphers _ciphers..._ ++
*Default*: Go version-defined set of 'secure ciphers', ordered by hardware
performance
List of supported cipher suites, in preference order. Not used with TLS 1.3.
Valid values:
- RSA-WITH-RC4128-SHA
- RSA-WITH-3DES-EDE-CBC-SHA
- RSA-WITH-AES128-CBC-SHA
- RSA-WITH-AES256-CBC-SHA
- RSA-WITH-AES128-CBC-SHA256
- RSA-WITH-AES128-GCM-SHA256
- RSA-WITH-AES256-GCM-SHA384
- ECDHE-ECDSA-WITH-RC4128-SHA
- ECDHE-ECDSA-WITH-AES128-CBC-SHA
- ECDHE-ECDSA-WITH-AES256-CBC-SHA
- ECDHE-RSA-WITH-RC4128-SHA
- ECDHE-RSA-WITH-3DES-EDE-CBC-SHA
- ECDHE-RSA-WITH-AES128-CBC-SHA
- ECDHE-RSA-WITH-AES256-CBC-SHA
- ECDHE-ECDSA-WITH-AES128-CBC-SHA256
- ECDHE-RSA-WITH-AES128-CBC-SHA256
- ECDHE-RSA-WITH-AES128-GCM-SHA256
- ECDHE-ECDSA-WITH-AES128-GCM-SHA256
- ECDHE-RSA-WITH-AES256-GCM-SHA384
- ECDHE-ECDSA-WITH-AES256-GCM-SHA384
- ECDHE-RSA-WITH-CHACHA20-POLY1305
- ECDHE-ECDSA-WITH-CHACHA20-POLY1305
*Syntax*: curve _curves..._ ++
*Default*: defined by Go version
The elliptic curves that will be used in an ECDHE handshake, in preference
order.
Valid values: p256, p384, p521, X25519.
# TLS client configuration
tls_client directive allows to customize behavior of TLS client implementation,
notably adjusting minimal and maximal TLS versions and allowed cipher suites,
enabling TLS client authentication.
```
tls_client {
protocols tls1.2 tls1.3
ciphers ...
curve X25519
root_ca /etc/ssl/cert.pem
cert /etc/ssl/private/maddy-client.pem
key /etc/ssl/private/maddy-client.pem
}
```
*Syntax*: ++
protocols _min_version_ _max_version_ ++
protocols _version_ ++
*Default*: tls1.0 tls1.3
Minimum/maximum accepted TLS version. If only one value is specified, it will
be the only one usable version.
Valid values are: tls1.0, tls1.1, tls1.2, tls1.3
*Syntax*: ciphers _ciphers..._ ++
*Default*: Go version-defined set of 'secure ciphers', ordered by hardware
performance
List of supported cipher suites, in preference order. Not used with TLS 1.3.
See TLS server configuration for list of supported values.
*Syntax*: curve _curves..._ ++
*Default*: defined by Go version
The elliptic curves that will be used in an ECDHE handshake, in preference
order.
Valid values: p256, p384, p521, X25519.
*Syntax*: root_ca _paths..._ ++
*Default*: system CA pool
List of files with PEM-encoded CA certificates to use when verifying
server certificates.
*Syntax*: ++
cert _cert_path_ ++
key _key_path_ ++
*Default*: not specified
Present the specified certificate when server requests a client certificate.
Files should use PEM format. Both directives should be specified.
# Automatic certificate management via ACME
```
tls.loader.acme {
debug off
hostname example.maddy.invalid
store_path /var/lib/maddy/acme
ca https://acme-v02.api.letsencrypt.org/directory
test_ca https://acme-staging-v02.api.letsencrypt.org/directory
email test@maddy.invalid
agreed off
challenge dns-01
dns ...
}
```
Maddy supports obtaining certificates using ACME protocol.
To use it, create a configuration name for tls.loader.acme
and reference it from endpoints that should use automatically
configured certificates:
```
tls.loader.acme local_tls {
email put-your-email-here@example.org
agreed # indicate your agreement with Let's Encrypt ToS
challenge dns-01
}
smtp tcp://127.0.0.1:25 {
tls &local_tls
...
}
```
Currently the only supported challenge is dns-01 one therefore
you also need to configure the DNS provider:
```
tls.loader.acme local_tls {
email maddy-acme@example.org
agreed
challenge dns-01
dns PROVIDER_NAME {
...
}
}
```
See below for supported providers and necessary configuration
for each.
## Configuration directives
*Syntax:* debug _boolean_ ++
*Default:* global directive value
Enable debug logging.
*Syntax:* hostname _str_ ++
*Default:* global directive value
Domain name to issue certificate for. Required.
*Syntax:* store_path _path_ ++
*Default:* state_dir/acme
Where to store issued certificates and associated metadata.
Currently only filesystem-based store is supported.
*Syntax:* ca _url_ ++
*Default:* Let's Encrypt production CA
URL of ACME directory to use.
*Syntax:* test_ca _url_ ++
*Default:* Let's Encrypt staging CA
URL of ACME directory to use for retries should
primary CA fail.
maddy will keep attempting to issues certificates
using test_ca until it succeeds then it will switch
back to the one configured via 'ca' option.
This avoids rate limit issues with production CA.
*Syntax:* email _str_ ++
*Default:* not set
Email to pass while registering an ACME account.
*Syntax:* agreed _boolean_ ++
*Default:* false
Whether you agreed to ToS of the CA service you are using.
*Syntax:* challenge dns-01 ++
*Default:* not set
Challenge(s) to use while performing domain verification.
## DNS providers
Support for some providers is not provided by standard builds.
To be able to use these, you need to compile maddy
with "libdns_PROVIDER" build tag.
E.g.
```
./build.sh -tags 'libdns_googleclouddns'
```
- gandi
```
dns gandi {
api_token "token"
}
```
- digitalocean
```
dns digitalocean {
api_token "..."
}
```
- cloudflare
See https://github.com/libdns/cloudflare#authenticating
```
dns cloudflare {
api_token "..."
}
```
- vultr
```
dns vultr {
api_token "..."
}
```
- hetzner
```
dns hetzner {
api_token "..."
}
```
- namecheap
```
dns namecheap {
api_key "..."
api_username "..."
# optional: API endpoint, production one is used if not set.
endpoint "https://api.namecheap.com/xml.response"
# optional: your public IP, discovered using icanhazip.com if not set
client_ip 1.2.3.4
}
```
- googleclouddns (non-default)
```
dns googleclouddns {
project "project_id"
service_account_json "path"
}
```
- route53 (non-default)
```
dns route53 {
secret_access_key "..."
access_key_id "..."
# or use environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
}
```
- leaseweb (non-default)
```
dns leaseweb {
api_key "key"
}
```
- metaname (non-default)
```
dns metaname {
api_key "key"
account_ref "reference"
}
```
- alidns (non-default)
```
dns alidns {
key_id "..."
key_secret "..."
}
```
- namedotcom (non-default)
```
dns namedotcom {
user "..."
token "..."
}
```

View file

@ -1,228 +0,0 @@
maddy(1) "maddy mail server" "maddy reference documentation"
; TITLE Introduction
# Modules
maddy is built of many small components called "modules". Each module does one
certain well-defined task. Modules can be connected to each other in arbitrary
ways to achieve wanted functionality. Default configuration file defines
set of modules that together implement typical email server stack.
To specify the module that should be used by another module for something, look
for configuration directives with "module reference" argument. Then
put the module name as an argument for it. Optionally, if referenced module
needs that, put additional arguments after the name. You can also put a
configuration block with additional directives specifing the module
configuration.
Here are some examples:
```
smtp ... {
# Deliver messages to the 'dummy' module with the default configuration.
deliver_to dummy
# Deliver messages to the 'target.smtp' module with
# 'tcp://127.0.0.1:1125' argument as a configuration.
deliver_to smtp tcp://127.0.0.1:1125
# Deliver messages to the 'queue' module with the specified configuration.
deliver_to queue {
target ...
max_tries 10
}
}
```
Additionally, module configuration can be placed in a separate named block
at the top-level and merely referenced by its name where it is needed.
Here is the example:
```
storage.imapsql local_mailboxes {
driver sqlite3
dsn all.db
}
smtp ... {
deliver_to &local_mailboxes
}
```
It is recommended to use this syntax for modules that are 'expensive' to
initialize such as storage backends and authentication providers.
For top-level configuration block definition, syntax is as follows:
```
namespace.module_name config_block_name... {
module_configuration
}
```
If config_block_name is omitted, it will be the same as module_name. Multiple
names can be specified. All names must be unique.
Note the "storage." prefix. The actual module name is this and includes
"namespace". It is a little cheating to make more concise names and can
be omitted when you reference the module where it is used since it can
be implied (e.g. putting module reference in "check{}" likely means you want
something with "check." prefix)
Usual module arguments can't be specified when using this syntax, however,
modules usually provide explicit directives that allow to specify the needed
values. For example 'sql sqlite3 all.db' is equivalent to
```
storage.imapsql {
driver sqlite3
dsn all.db
}
```
# Reference documentation conventions
## Syntax descriptions for directives
Underlined values are placeholders and should be replaced by your values.
_boolean_ is either 'yes' or 'no' string.
Ellipsis (_smth..._) means that multiple values can be specified
Multiple values listed with '|' (pipe) separator mean that any of them
can be used.
# Global directives
These directives applied for all configuration blocks that don't override it.
*Syntax*: state_dir _path_ ++
*Default*: /var/lib/maddy
The path to the state directory. This directory will be used to store all
persistent data and should be writable.
*Syntax*: runtime_dir _path_ ++
*Default*: /run/maddy
The path to the runtime directory. Used for Unix sockets and other temporary
objects. Should be writable.
*Syntax*: hostname _domain_ ++
*Default*: not specified
Internet hostname of this mail server. Typicall FQDN is used. It is recommended
to make sure domain specified here resolved to the public IP of the server.
*Syntax*: autogenerated_msg_domain _domain_ ++
*Default*: not specified
Domain that is used in From field for auto-generated messages (such as Delivery
Status Notifications).
*Syntax*: ++
tls file _cert_file_ _pkey_file_ ++
tls _module reference_ ++
tls off ++
*Default*: not specified
Default TLS certificate to use for all endpoints.
Must be present in either all endpoint modules configuration blocks or as
global directive.
You can also specify other configuration options such as cipher suites and TLS
version. See maddy-tls(5) for details. maddy uses reasonable
cipher suites and TLS versions by default so you generally don't have to worry
about it.
*Syntax*: tls_client { ... } ++
*Default*: not specified
This is optional block that specifies various TLS-related options to use when
making outbound connections. See TLS client configuration for details on
directives that can be used in it. maddy uses reasonable cipher suites and TLS
versions by default so you generally don't have to worry about it.
*Syntax*: ++
log _targets..._ ++
log off ++
*Default*: stderr
Write log to one of more "targets".
The target can be one or the following:
- stderr
Write logs to stderr.
- stderr_ts
Write logs to stderr with timestamps.
- syslog
Send logs to the local syslog daemon.
- _file path_
Write (append) logs to file.
Example:
```
log syslog /var/log/maddy.log
```
*Note:* Maddy does not perform log files rotation, this is the job of the
logrotate daemon. Send SIGUSR1 to maddy process to make it reopen log files.
*Syntax*: debug _boolean_ ++
*Default*: no
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.
# Prometheus/OpenMetrics endpoint
```
openmetrics tcp://127.0.0.1:9749 { }
```
This will enable HTTP listener that will serve telemetry in OpenMetrics format.
(It is compatible with Prometheus).
See openmetrics.md documentation page the list of metrics exposed.
# Signals
*SIGTERM, SIGINT, SIGHUP*
Stop the server process gracefully. Send the signal second time to force
immediate shutdown (likely unclean).
*SIGUSR1*
Reopen log files, if any are used.
*SIGUSR2*
Reload some files from disk, including alias mappings and TLS certificates.
This does not include the main configuration, though.
# Authors
Maintained by Max Mazurov <fox.cpp@disroot.org>. Project includes contributions
made by other people.
Source code is available at https://github.com/foxcpp/maddy.
# See also
*maddy-config*(5) - Detailed configuration syntax description ++
*maddy-imap*(5) - IMAP endpoint module reference ++
*maddy-smtp*(5) - SMTP & Submission endpoint module reference ++
*maddy-targets*(5) - Delivery targets reference ++
*maddy-storage*(5) - Storage modules reference ++
*maddy-auth*(5) - Authentication modules reference ++
*maddy-filters*(5) - Message filtering modules reference ++
*maddy-tables*(5) - Table modules reference ++
*maddy-tls*(5) - Advanced TLS client & server configuration

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
**Syntax**: endpoint _schema://address_ <br>
**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,47 @@
# System command
auth.external module for authentication using external helper binary. It looks for binary
named maddy-auth-helper in $PATH and libexecdir and uses it for authentication
using username/password pair.
The protocol is very simple:
Program is launched for each authentication. Username and password are written
to stdin, adding \\n to the end. If binary exits with 0 status code -
authentication is considered successful. If the status code is 1 -
authentication is failed. If the status code is 2 - another unrelated error has
happened. Additional information should be written to stderr.
```
auth.external {
helper /usr/bin/ldap-helper
perdomain no
domains example.org
}
```
## Configuration directives
**Syntax**: helper _file\_path\_
Location of the helper binary. **Required.**
**Syntax**: perdomain _boolean_ <br>
**Default**: no
Don't remove domain part of username when authenticating and require it to be
present. Can be used if you want user@domain1 and user@domain2 to be different
accounts.
**Syntax**: domains _domains..._ <br>
**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.

113
docs/reference/auth/ldap.md Normal file
View file

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

View file

@ -0,0 +1,44 @@
# PAM
auth.pam module implements authentication using libpam. Alternatively it can be configured to
use helper binary like auth.external module does.
maddy should be built with libpam build tag to use this module without
'use\_helper' directive.
```
go get -tags 'libpam' ...
```
```
auth.pam {
debug no
use_helper no
}
```
## Configuration directives
**Syntax**: debug _boolean_ <br>
**Default**: no
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.
**Syntax**: use\_helper _boolean_ <br>
**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 'maddyctl hash' command to generate suitable values.
See 'maddyctl hash --help' for details.
## maddyctl creds
If the underlying table is a "mutable" table (see maddy-tables(5)) then
the 'maddyctl 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,42 @@
# Separate username and password lookup
auth.plain\_separate module implements authentication using username:password pairs but can
use zero or more "table modules" (maddy-tables(5)) and one or more
authentication providers to verify credentials.
```
auth.plain_separate {
user ...
user ...
...
pass ...
pass ...
...
}
```
How it works:
- Initial username input is normalized using PRECIS UsernameCaseMapped profile.
- Each table specified with the 'user' directive looked up using normalized
username. If match is not found in any table, authentication fails.
- Each authentication provider specified with the 'pass' directive is tried.
If authentication with all providers fails - an error is returned.
## Configuration directives
***Syntax:*** user _table module\_
Configuration block for any module from maddy-tables(5) can be used here.
Example:
```
user file /etc/maddy/allowed_users
```
***Syntax:*** pass _auth provider\_
Configuration block for any auth. provider module can be used here, even
'plain\_split' itself.
The used auth. provider must provide username:password pair-based
authentication.

View file

@ -0,0 +1,36 @@
# /etc/shadow
auth.shadow module implements authentication by reading /etc/shadow. Alternatively it can be
configured to use helper binary like auth.external does.
```
auth.shadow {
debug no
use_helper no
}
```
## Configuration directives
**Syntax**: debug _boolean_ <br>
**Default**: no
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.
**Syntax**: use\_helper _boolean_ <br>
**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
```

22
docs/reference/blob/fs.md Normal file
View file

@ -0,0 +1,22 @@
# Filesystem
This module stores message bodies in a file system directory.
```
storage.blob.fs {
root <directory>
}
```
```
storage.blob.fs <directory>
```
## Configuration directives
**Syntax:** root _path_ <br>
**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.

71
docs/reference/blob/s3.md Normal file
View file

@ -0,0 +1,71 @@
# 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/
}
```
Example:
```
storage.imapsql local_mailboxes {
...
msg_store s3 {
endpoint s3.amazonaws.com
access_key "..."
secret_key "..."
bucket maddy-messages
region us-west-2
}
}
```
## Configuration directives
**Syntax:** endpoint _address:port\_
REQUIRED.
Root S3 endpoint. e.g. s3.amazonaws.com
**Syntax:** secure _boolean_ <br>
**Default:** yes
Whether TLS should be used.
**Syntax:** access\_key _string_ <br>
**Syntax:** secret\_key _string\_
REQUIRED.
Static S3 credentials.
**Syntax:** bucket _name\_
REQUIRED.
S3 bucket name. The bucket must exist and
be read-writable.
**Syntax:** region _string_ <br>
**Default:** not set
S3 bucket location. May be called "endpoint"
in some manuals.
**Syntax:** object\_prefix _string_ <br>
**Default:** empty string
String to add to all keys stored by maddy.
Can be useful when S3 is used as a file system.

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,87 @@
# MAIL FROM and From authorization
Module check.authorize_sender verifies that envelope and header sender addresses belong
to the authenticated user. Address ownership is established via table
that maps each user account to a email address it is allowed to use.
There are some special cases, see user\_to\_email description below.
```
check.authorize_sender {
prepare_email identity
user_to_email identity
check_header yes
unauth_action reject
no_match_action reject
malformed_action reject
err_action reject
auth_normalize precis_casefold_email
from_normalize precis_casefold_email
}
```
```
check {
authorize_sender { ... }
}
```
## Configuration directives
**Syntax:** user\_to\_email _table_ <br>
**Default:** identity
Table to use for lookups. Result of the lookup should contain either the
domain name, the full email address or "*" string. If it is just domain - user
will be allowed to use any mailbox within a domain as a sender address.
If result contains "*" - user will be allowed to use any address.
**Syntax:** check\_header _boolean_ <br>
**Default:** yes
Whether to verify header sender in addition to envelope.
Either Sender or From field value should match the
authorization identity.
**Syntax:** unauth\_action _action_ <br>
**Default:** reject
What to do if the user is not authenticated at all.
**Syntax:** no\_match\_action _action_ <br>
**Default:** reject
What to do if user is not allowed to use the sender address specified.
**Syntax:** malformed\_action _action_ <br>
**Default:** reject
What to do if From or Sender header fields contain malformed values.
**Syntax:** err\_action _action_ <br>
**Default:** reject
What to do if error happens during prepare\_email or user\_to\_email lookup.
**Syntax:** auth\_normalize _action_ <br>
**Default:** precis\_casefold\_email
Normalization function to apply to authorization username before
further processing.
Available options:
- precis\_casefold\_email PRECIS UsernameCaseMapped profile + U-labels form for domain
- precis\_casefold PRECIS UsernameCaseMapped profile for the entire string
- precis\_email PRECIS UsernameCasePreserved profile + U-labels form for domain
- precis PRECIS UsernameCasePreserved profile for the entire string
- casefold Convert to lower case
- noop Nothing
**Syntax:** from\_normalize _action_ <br>
**Default:** precis\_casefold\_email
Normalization function to apply to email addresses before
further processing.
Available options are same as for auth\_normalize.

View file

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

View file

@ -0,0 +1,55 @@
# DKIM
This is the check module that performs verification of the DKIM signatures
present on the incoming messages.
## Configuration directives
```
check.dkim {
debug no
required_fields From Subject
allow_body_subset no
no_sig_action ignore
broken_sig_action ignore
fail_open no
}
```
**Syntax**: debug _boolean_ <br>
**Default**: global directive value
Log both successfull and unsuccessful check executions instead of just
unsuccessful.
**Syntax**: required\_fields _string..._ <br>
**Default**: From Subject
Header fields that should be included in each signature. If signature
lacks any field listed in that directive, it will be considered invalid.
Note that From is always required to be signed, even if it is not included in
this directive.
**Syntax**: no\_sig\_action _action_ <br>
**Default**: ignore (recommended by RFC 6376)
Action to take when message without any signature is received.
Note that DMARC policy of the sender domain can request more strict handling of
missing DKIM signatures.
**Syntax**: broken\_sig\_action _action_ <br>
**Default**: ignore (recommended by RFC 6376)
Action to take when there are not valid signatures in a message.
Note that DMARC policy of the sender domain can request more strict handling of
broken DKIM signatures.
**Syntax**: fail\_open _boolean_ <br>
**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,156 @@
# DNSBL lookup
The check.dnsbl module implements checking of source IP and hostnames against a set
of DNS-based Blackhole lists (DNSBLs).
Its configuration consists of module configuration directives and a set
of blocks specifing lists to use and kind of lookups to perform on them.
```
check.dnsbl {
debug no
check_early no
quarantine_threshold 1
reject_threshold 1
# Lists configuration example.
dnsbl.example.org {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
score 1
}
hsrbl.example.org {
client_ipv4 no
client_ipv6 no
ehlo yes
mailfrom yes
score 1
}
}
```
## Arguments
Arguments specify the list of IP-based BLs to use.
The following configurations are equivalent.
```
check {
dnsbl dnsbl.example.org dnsbl2.example.org
}
```
```
check {
dnsbl {
dnsbl.example.org dnsbl2.example.org {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
score 1
}
}
}
```
## Configuration directives
**Syntax**: debug _boolean_ <br>
**Default**: global directive value
Enable verbose logging.
**Syntax**: check\_early _boolean_ <br>
**Default**: no
Check BLs before mail delivery starts and silently reject blacklisted clients.
For this to work correctly, check should not be used in source/destination
pipeline block.
In particular, this means:
- No logging is done for rejected messages.
- No action is taken if quarantine\_threshold is hit, only reject\_threshold
applies.
- defer\_sender\_reject from SMTP configuration takes no effect.
- MAIL FROM is not checked, even if specified.
If you often get hit by spam attacks, it is recommended to enable this
setting to save server resources.
**Syntax**: quarantine\_threshold _integer_ <br>
**Default**: 1
DNSBL score needed (equals-or-higher) to quarantine the message.
**Syntax**: reject\_threshold _integer_ <br>
**Default**: 9999
DNSBL score needed (equals-or-higher) to reject the message.
## List configuration
```
dnsbl.example.org dnsbl.example.com {
client_ipv4 yes
client_ipv6 no
ehlo no
mailfrom no
responses 127.0.0.1/24
score 1
}
```
Directive name and arguments specify the actual DNS zone to query when checking
the list. Using multiple arguments is equivalent to specifying the same
configuration separately for each list.
**Syntax**: client\_ipv4 _boolean_ <br>
**Default**: yes
Whether to check address of the IPv4 clients against the list.
**Syntax**: client\_ipv6 _boolean_ <br>
**Default**: yes
Whether to check address of the IPv6 clients against the list.
**Syntax**: ehlo _boolean_ <br>
**Default**: no
Whether to check hostname specified n the HELO/EHLO command
against the list.
This works correctly only with domain-based DNSBLs.
**Syntax**: mailfrom _boolean_ <br>
**Default**: no
Whether to check domain part of the MAIL FROM address against the list.
This works correctly only with domain-based DNSBLs.
**Syntax**: responses _cidr|ip..._ <br>
**Default**: 127.0.0.1/24
IP networks (in CIDR notation) or addresses to permit in list lookup results.
Addresses not matching any entry in this directives will be ignored.
**Syntax**: score _integer_ <br>
**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,47 @@
# Milter client
The 'milter' implements subset of Sendmail's milter protocol that can be used
to integrate external software with maddy.
maddy implements version 6 of the protocol, older versions are
not supported.
Notable limitations of protocol implementation in maddy include:
1. Changes of envelope sender address are not supported
2. Removal and addition of envelope recipients is not supported
3. Removal and replacement of header fields is not supported
4. Headers fields can be inserted only on top
5. Milter does not receive some "macros" provided by sendmail.
Restrictions 1 and 2 are inherent to the maddy checks interface and cannot be
removed without major changes to it. Restrictions 3, 4 and 5 are temporary due to
incomplete implementation.
```
check.milter {
endpoint <endpoint>
fail_open false
}
milter <endpoint>
```
## Arguments
When defined inline, the first argument specifies endpoint to access milter
via. See below.
## Configuration directives
***Syntax:*** endpoint _scheme://path_ <br>
***Default:*** not set
Specifies milter protocol endpoint to use.
The endpoit is specified in standard URL-like format:
'tcp://127.0.0.1:6669' or 'unix:///var/lib/milter/filter.sock'
***Syntax:*** fail\_open _boolean_ <br>
***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,43 @@
# Misc checks
## Configuration directives
Following directives are defined for all modules listed below.
**Syntax**: <br>
fail\_action ignore <br>
fail\_action reject <br>
fail\_action quarantine <br>
**Default**: quarantine
Action to take when check fails. See Check actions for details.
**Syntax**: debug _boolean_ <br>
**Default**: global directive value
Log both sucessfull and unsucessfull check executions instead of just
unsucessfull.
## require\_mx\_record
Check that domain in MAIL FROM command does have a MX record and none of them
are "null" (contain a single dot as the host).
By default, quarantines messages coming from servers missing MX records,
use 'fail\_action' directive to change that.
## require\_matching\_rdns
Check that source server IP does have a PTR record point to the domain
specified in EHLO/HELO command.
By default, quarantines messages coming from servers with mismatched or missing
PTR record, use 'fail\_action' directive to change that.
## require\_tls
Check that the source server is connected via TLS; either directly, or by using
the STARTTLS command.
By default, rejects messages coming from unencrypted servers. Use the
'fail\_action' directive to change that.

View file

@ -0,0 +1,79 @@
# rspamd
The 'rspamd' module implements message filtering by contacting the rspamd
server via HTTP API.
```
check.rspamd {
tls_client { ... }
api_path http://127.0.0.1:11333
settings_id whatever
tag maddy
hostname mx.example.org
io_error_action ignore
error_resp_action ignore
add_header_action quarantine
rewrite_subj_action quarantine
flags pass_all
}
rspamd http://127.0.0.1:11333
```
## Configuration directives
**Syntax:** tls\_client { ... } <br>
**Default:** not set
Configure TLS client if HTTPS is used. See [TLS configuration / Client](/reference/tls/#client) for details.
**Syntax:** api\_path _url_ <br>
**Default:** http://127.0.0.1:11333
URL of HTTP API endpoint. Supports both HTTP and HTTPS and can include
path element.
**Syntax:** settings\_id _string_ <br>
**Default:** not set
Settings ID to pass to the server.
**Syntax:** tag _string_ <br>
**Default:** maddy
Value to send in MTA-Tag header field.
**Syntax:** hostname _string_ <br>
**Default:** value of global directive
Value to send in MTA-Name header field.
**Syntax:** io\_error\_action _action_ <br>
**Default:** ignore
Action to take in case of inability to contact the rspamd server.
**Syntax:** error\_resp\_action _action_ <br>
**Default:** ignore
Action to take in case of 5xx or 4xx response received from the rspamd server.
**Syntax:** add\_header\_action _action_ <br>
**Default:** quarantine
Action to take when rspamd requests to "add header".
X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.
**Syntax:** rewrite\_subj\_action _action_ <br>
**Default:** quarantine
Action to take when rspamd requests to "rewrite subject".
X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.
**Syntax:** flags _string list..._ <br>
**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,83 @@
# SPF
check.spf the check module that verifies whether IP address of the client is
authorized to send messages for domain in MAIL FROM address.
SPF statuses are mapped to maddy check actions in a way
specified by \*_action directives. By default, SPF failure
results in the message being quarantined and errors (both permanent and
temporary) cause message to be rejected.
Authentication-Results field is generated irregardless of status.
## DMARC override
It is recommended by the DMARC standard to don't fail delivery based solely on
SPF policy and always check DMARC policy and take action based on it.
If enforce\_early is no, check.spf module will not take any action on SPF
policy failure if sender domain does have a DMARC record with 'quarantine' or
'reject' policy. Instead it will rely on DMARC support to take necesary
actions using SPF results as an input.
Disabling enforce\_early without enabling DMARC support will make SPF policies
no-op and is considered insecure.
## Configuration directives
```
check.spf {
debug no
enforce_early no
fail_action quarantine
softfail_action ignore
permerr_action reject
temperr_action reject
}
```
**Syntax**: debug _boolean_ <br>
**Default**: global directive value
Enable verbose logging for check.spf.
**Syntax**: enforce\_early _boolean_ <br>
**Default**: no
Make policy decision on MAIL FROM stage (before the message body is received).
This makes it impossible to apply DMARC override (see above).
**Syntax**: none\_action reject|qurantine|ignore <br>
**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.
**Syntax**: neutral\_action reject|qurantine|ignore <br>
**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.
**Syntax**: fail\_action reject|qurantine|ignore <br>
**Default**: quarantine
Action to take when SPF policy evaluates to a 'fail' result.
**Syntax**: softfail\_action reject|qurantine|ignore <br>
**Default**: ignore
Action to take when SPF policy evaluates to a 'softfail' result.
**Syntax**: permerr\_action reject|qurantine|ignore <br>
**Default**: reject
Action to take when SPF policy evaluates to a 'permerror' result.
**Syntax**: temperr\_action reject|qurantine|ignore <br>
**Default**: reject
Action to take when SPF policy evaluates to a 'temperror' result.

View file

@ -1,6 +1,7 @@
maddy-config(5) "maddy mail server" "maddy reference documentation"
# Configuration files syntax
; TITLE 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.
@ -185,7 +186,7 @@ Also note that the following is not valid, unlike Duration values syntax:
Maddy configuration uses URL-like syntax to specify network addresses.
- unix://file_path
- unix://file\_path
Unix domain socket. Relative paths are relative to runtime directory
(/run/maddy).
@ -202,3 +203,4 @@ 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,65 @@
# IMAP4rev1 endpoint
Module 'imap' is a listener that implements IMAP4rev1 protocol and provides
access to local messages storage specified by 'storage' directive.
In most cases, local storage modules will auto-create accounts when they are
accessed via IMAP. This relies on authentication provider used by IMAP endpoint
to provide what essentially is access control. There is a caveat, however: this
auto-creation will not happen when delivering incoming messages via SMTP as
there is no authentication to confirm that this account should indeed be
created.
## Configuration directives
```
imap tcp://0.0.0.0:143 tls://0.0.0.0:993 {
tls /etc/ssl/private/cert.pem /etc/ssl/private/pkey.key
io_debug no
debug no
insecure_auth no
auth pam
storage &local_mailboxes
}
```
**Syntax**: tls _certificate\_path_ _key\_path_ { ... } <br>
**Default**: global directive value
TLS certificate & key to use. Fine-tuning of other TLS properties is possible
by specifing a configuration block and options inside it:
```
tls cert.crt key.key {
protocols tls1.2 tls1.3
}
```
See [TLS configuration / Server](/reference/tls/#server-side) for details.
**Syntax**: io\_debug _boolean_ <br>
**Default**: no
Write all commands and responses to stderr.
**Syntax**: io\_errors _boolean_ <br>
**Default**: no
Log I/O errors.
**Syntax**: debug _boolean_ <br>
**Default**: global directive value
Enable verbose logging.
**Syntax**: insecure\_auth _boolean_ <br>
**Default**: no (yes if TLS is disabled)
**Syntax**: auth _module\_reference\_
Use the specified module for authentication.
**Required.**
**Syntax**: storage _module\_reference\_
Use the specified module for message storage.
**Required.**

View file

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

View file

@ -0,0 +1,94 @@
# Global configuration directives
These directives can be specified outside of any
configuration blocks and they are applied to all modules.
Some directives can be overridden on per-module basis (e.g. hostname).
**Syntax**: state\_dir _path_ <br>
**Default**: /var/lib/maddy
The path to the state directory. This directory will be used to store all
persistent data and should be writable.
**Syntax**: runtime\_dir _path_ <br>
**Default**: /run/maddy
The path to the runtime directory. Used for Unix sockets and other temporary
objects. Should be writable.
**Syntax**: hostname _domain_ <br>
**Default**: not specified
Internet hostname of this mail server. Typicall FQDN is used. It is recommended
to make sure domain specified here resolved to the public IP of the server.
**Syntax**: autogenerated\_msg\_domain _domain_ <br>
**Default**: not specified
Domain that is used in From field for auto-generated messages (such as Delivery
Status Notifications).
**Syntax**: <br>
tls file _cert\_file_ _pkey\_file_ <br>
tls _module reference_ <br>
tls off <br>
**Default**: not specified
Default TLS certificate to use for all endpoints.
Must be present in either all endpoint modules configuration blocks or as
global directive.
You can also specify other configuration options such as cipher suites and TLS
version. See maddy-tls(5) for details. maddy uses reasonable
cipher suites and TLS versions by default so you generally don't have to worry
about it.
**Syntax**: tls\_client { ... } <br>
**Default**: not specified
This is optional block that specifies various TLS-related options to use when
making outbound connections. See TLS client configuration for details on
directives that can be used in it. maddy uses reasonable cipher suites and TLS
versions by default so you generally don't have to worry about it.
**Syntax**: <br>
log _targets..._ <br>
log off <br>
**Default**: stderr
Write log to one of more "targets".
The target can be one or the following:
- stderr
Write logs to stderr.
- stderr\_ts
Write logs to stderr with timestamps.
- syslog
Send logs to the local syslog daemon.
- _file path_
Write (append) logs to file.
Example:
```
log syslog /var/log/maddy.log
```
**Note:** Maddy does not perform log files rotation, this is the job of the
logrotate daemon. Send SIGUSR1 to maddy process to make it reopen log files.
**Syntax**: debug _boolean_ <br>
**Default**: no
Enable verbose logging for all modules. You don't need that unless you are
reporting a bug.

View file

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

View file

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

76
docs/reference/modules.md Normal file
View file

@ -0,0 +1,76 @@
# Modules introduction
maddy is built of many small components called "modules". Each module does one
certain well-defined task. Modules can be connected to each other in arbitrary
ways to achieve wanted functionality. Default configuration file defines
set of modules that together implement typical email server stack.
To specify the module that should be used by another module for something, look
for configuration directives with "module reference" argument. Then
put the module name as an argument for it. Optionally, if referenced module
needs that, put additional arguments after the name. You can also put a
configuration block with additional directives specifing the module
configuration.
Here are some examples:
```
smtp ... {
# Deliver messages to the 'dummy' module with the default configuration.
deliver_to dummy
# Deliver messages to the 'target.smtp' module with
# 'tcp://127.0.0.1:1125' argument as a configuration.
deliver_to smtp tcp://127.0.0.1:1125
# Deliver messages to the 'queue' module with the specified configuration.
deliver_to queue {
target ...
max_tries 10
}
}
```
Additionally, module configuration can be placed in a separate named block
at the top-level and referenced by its name where it is needed.
Here is the example:
```
storage.imapsql local_mailboxes {
driver sqlite3
dsn all.db
}
smtp ... {
deliver_to &local_mailboxes
}
```
It is recommended to use this syntax for modules that are 'expensive' to
initialize such as storage backends and authentication providers.
For top-level configuration block definition, syntax is as follows:
```
namespace.module_name config_block_name... {
module_configuration
}
```
If config\_block\_name is omitted, it will be the same as module\_name. Multiple
names can be specified. All names must be unique.
Note the "storage." prefix. This is the actual module name and includes
"namespace". It is a little cheating to make more concise names and can
be omitted when you reference the module where it is used since it can
be implied (e.g. putting module reference in "check{}" likely means you want
something with "check." prefix)
Usual module arguments can't be specified when using this syntax, however,
modules usually provide explicit directives that allow to specify the needed
values. For example 'sql sqlite3 all.db' is equivalent to
```
storage.imapsql {
driver sqlite3
dsn all.db
}
```

View file

@ -0,0 +1,384 @@
# SMTP message routing (pipeline)
# Message pipeline
Message pipeline is a set of module references and associated rules that
describe how to handle messages.
The pipeline is responsible for
- Running message filters (called "checks"), (e.g. DKIM signature verification,
DNSBL lookup and so on).
- Running message modifiers (e.g. DKIM signature creation).
- Assocating each message recipient with one or more delivery targets.
Delivery target is a module that does final processing (delivery) of the
message.
Message handling flow is as follows:
- Execute checks referenced in top-level 'check' blocks (if any)
- Execute modifiers referenced in top-level 'modify' blocks (if any)
- If there are 'source' blocks - select one that matches message sender (as
specified in MAIL FROM). If there are no 'source' blocks - entire
configuration is assumed to be the 'default\_source' block.
- Execute checks referenced in 'check' blocks inside selected 'source' block
(if any).
- Execute modifiers referenced in 'modify' blocks inside selected 'source'
block (if any).
Then, for each recipient:
- Select 'destination' block that matches it. If there are
no 'destination' blocks - entire used 'source' block is interpreted as if it
was a 'default\_destination' block.
- Execute checks referenced in 'check' block inside selected 'destination' block
(if any).
- Execute modifiers referenced in 'modify' block inside selected 'destination'
block (if any).
- If used block contains 'reject' directive - reject the recipient with
specified SMTP status code.
- If used block contains 'deliver\_to' directive - pass the message to the
specified target module. Only recipients that are handled
by used block are visible to the target.
Each recipient is handled only by a single 'destination' block, in case of
overlapping 'destination' - first one takes priority.
```
destination example.org {
deliver_to targetA
}
destination example.org { # ambiguous and thus not allowed
deliver_to targetB
}
```
Same goes for 'source' blocks, each message is handled only by a single block.
Each recipient block should contain at least one 'deliver\_to' directive or
'reject' directive. If 'destination' blocks are used, then
'default\_destination' block should also be used to specify behavior for
unmatched recipients. Same goes for source blocks, 'default\_source' should be
used if 'source' is used.
That is, pipeline configuration should explicitly specify behavior for each
possible sender/recipient combination.
Additionally, directives that specify final handling decision ('deliver\_to',
'reject') can't be used at the same level as source/destination rules.
Consider example:
```
destination example.org {
deliver_to local_mboxes
}
reject
```
It is not obvious whether 'reject' applies to all recipients or
just for non-example.org ones, hence this is not allowed.
Complete configuration example using all of the mentioned directives:
```
check {
# Run a check to make sure source SMTP server identification
# is legit.
require_matching_ehlo
}
# Messages coming from senders at example.org will be handled in
# accordance with the following configuration block.
source example.org {
# We are example.com, so deliver all messages with recipients
# at example.com to our local mailboxes.
destination example.com {
deliver_to &local_mailboxes
}
# We don't do anything with recipients at different domains
# because we are not an open relay, thus we reject them.
default_destination {
reject 521 5.0.0 "User not local"
}
}
# We do our business only with example.org, so reject all
# other senders.
default_source {
reject
}
```
## Directives
**Syntax**: check _block name_ { ... } <br>
**Context**: pipeline configuration, source block, destination block
List of the module references for checks that should be executed on
messages handled by block where 'check' is placed in.
Note that message body checks placed in destination block are currently
ignored. Due to the way SMTP protocol is defined, they would cause message to
be rejected for all recipients which is not what you usually want when using
such configurations.
Example:
```
check {
# Reference implicitly defined default configuration for check.
require_matching_ehlo
# Inline definition of custom config.
require_source_mx {
# Configuration for require_source_mx goes here.
fail_action reject
}
}
```
It is also possible to define the block of checks at the top level
as "checks" module and reference it using & syntax. Example:
```
checks inbound_checks {
require_matching_ehlo
}
# ... somewhere else ...
{
...
check &inbound_checks
}
```
**Syntax**: modify { ... } <br>
**Default**: not specified <br>
**Context**: pipeline configuration, source block, destination block
List of the module references for modifiers that should be executed on
messages handled by block where 'modify' is placed in.
Message modifiers are similar to checks with the difference in that checks
purpose is to verify whether the message is legitimate and valid per local
policy, while modifier purpose is to post-process message and its metadata
before final delivery.
For example, modifier can replace recipient address to make message delivered
to the different mailbox or it can cryptographically sign outgoing message
(e.g. using DKIM). Some modifier can perform multiple unrelated modifications
on the message.
**Note**: Modifiers that affect source address can be used only globally or on
per-source basis, they will be no-op inside destination blocks. Modifiers that
affect the message header will affect it for all recipients.
It is also possible to define the block of modifiers at the top level
as "modiifers" module and reference it using & syntax. Example:
```
modifiers local_modifiers {
replace_rcpt file /etc/maddy/aliases
}
# ... somewhere else ...
{
...
modify &local_modifiers
}
```
**Syntax**: <br>
reject _smtp\_code_ _smtp\_enhanced\_code_ _error\_description_ <br>
reject _smtp\_code_ _smtp\_enhanced\_code_ <br>
reject _smtp\_code_ <br>
reject <br>
**Context**: destination block
Messages handled by the configuration block with this directive will be
rejected with the specified SMTP error.
If you aren't sure which codes to use, use 541 and 5.4.0 with your message or
just leave all arguments out, the error description will say "message is
rejected due to policy reasons" which is usually what you want to mean.
'reject' can't be used in the same block with 'deliver\_to' or
'destination/source' directives.
Example:
```
reject 541 5.4.0 "We don't like example.org, go away"
```
**Syntax**: deliver\_to _target-config-block_ <br>
**Context**: pipeline configuration, source block, destination block
Deliver the message to the referenced delivery target. What happens next is
defined solely by used target. If deliver\_to is used inside 'destination'
block, only matching recipients will be passed to the target.
**Syntax**: source\_in _table reference_ { ... } <br>
**Context**: pipeline configuration
Handle messages with envelope senders present in the specified table in
accordance with the specified configuration block.
Takes precedence over all 'sender' directives.
Example:
```
source_in file /etc/maddy/banned_addrs {
reject 550 5.7.0 "You are not welcome here"
}
source example.org {
...
}
...
```
See 'destination\_in' documentation for note about table configuration.
**Syntax**: source _rules..._ { ... } <br>
**Context**: pipeline configuration
Handle messages with MAIL FROM value (sender address) matching any of the rules
in accordance with the specified configuration block.
"Rule" is either a domain or a complete address. In case of overlapping
'rules', first one takes priority. Matching is case-insensitive.
Example:
```
# All messages coming from example.org domain will be delivered
# to local_mailboxes.
source example.org {
deliver_to &local_mailboxes
}
# Messages coming from different domains will be rejected.
default_source {
reject 521 5.0.0 "You were not invited"
}
```
**Syntax**: reroute { ... } <br>
**Context**: pipeline configuration, source block, destination block
This directive allows to make message routing decisions based on the
result of modifiers. The block can contain all pipeline directives and they
will be handled the same with the exception that source and destination rules
will use the final recipient and sender values (e.g. after all modifiers are
applied).
Here is the concrete example how it can be useful:
```
destination example.org {
modify {
replace_rcpt file /etc/maddy/aliases
}
reroute {
destination example.org {
deliver_to &local_mailboxes
}
default_destination {
deliver_to &remote_queue
}
}
}
```
This configuration allows to specify alias local addresses to remote ones
without being an open relay, since remote\_queue can be used only if remote
address was introduced as a result of rewrite of local address.
**WARNING**: If you have DMARC enabled (default), results generated by SPF
and DKIM checks inside a reroute block **will not** be considered in DMARC
evaluation.
**Syntax**: destination\_in _table reference_ { ... } <br>
**Context**: pipeline configuration, source block
Handle messages with envelope recipients present in the specified table in
accordance with the specified configuration block.
Takes precedence over all 'destination' directives.
Example:
```
destination_in file /etc/maddy/remote_addrs {
deliver_to smtp tcp://10.0.0.7:25
}
destination example.com {
deliver_to &local_mailboxes
}
...
```
Note that due to the syntax restrictions, it is not possible to specify
extended configuration for table module. E.g. this is not valid:
```
destination_in sql_table {
dsn ...
driver ...
} {
deliver_to whatever
}
```
In this case, configuration should be specified separately and be referneced
using '&' syntax:
```
table.sql_table remote_addrs {
dsn ...
driver ...
}
whatever {
destination_in &remote_addrs {
deliver_to whatever
}
}
```
**Syntax**: destination _rule..._ { ... } <br>
**Context**: pipeline configuration, source block
Handle messages with RCPT TO value (recipient address) matching any of the
rules in accordance with the specified configuration block.
"Rule" is either a domain or a complete address. Duplicate rules are not
allowed. Matching is case-insensitive.
Note that messages with multiple recipients are split into multiple messages if
they have recipients matched by multiple blocks. Each block will see the
message only with recipients matched by its rules.
Example:
```
# Messages with recipients at example.com domain will be
# delivered to local_mailboxes target.
destination example.com {
deliver_to &local_mailboxes
}
# Messages with other recipients will be rejected.
default_destination {
rejected 541 5.0.0 "User not local"
}
```
## Reusable pipeline snippets (msgpipeline module)
The message pipeline can be used independently of the SMTP module in other
contexts that require a delivery target via "msgpipeline" module.
Example:
```
msgpipeline local_routing {
destination whatever.com {
deliver_to dummy
}
}
# ... somewhere else ...
deliver_to &local_routing
```

View file

@ -0,0 +1,70 @@
# IMAP filters
Most storage backends support application of custom code late in delivery
process. As opposed to using SMTP pipeline modifiers or checks, it allows
modifying IMAP-specific message attributes. In particular, it allows
code to change target folder and add IMAP flags (keywords) to the message.
There is no way to reject message using IMAP filters, this should be done
eariler in SMTP pipeline logic. Quarantined messages are not processed
by IMAP filters and are unconditionally delivered to Junk folder (or other
folder with \Junk special-use attribute).
To use an IMAP filter, specify it in the 'imap\_filter' directive for the
used storage backend, like this:
```
storage.imapsql local_mailboxes {
...
imap_filter {
command /etc/maddy/sieve.sh {account_name}
}
}
```
## System command filter (imap.filter.command)
This filter is similar to check.command module
and runs a system command to obtain necessary information.
Usage:
```
command executable_name args... { }
```
Same as check.command, following placeholders are supported for command
arguments: {source\_ip}, {source\_host}, {source\_rdns}, {msg\_id}, {auth\_user},
{sender}. Note: placeholders
in command name are not processed to avoid possible command injection attacks.
Additionally, for imap.filter.command, {account\_name} placeholder is replaced
with effective IMAP account name, {rcpt_to}, {original_rcpt_to} provide
access to the SMTP envelope recipient (before and after any rewrites),
{subject} is replaced with the
Note that if you use provided systemd units on Linux, maddy executable is
sandboxed - all commands will be executed with heavily restricted filesystem
acccess and other privileges. Notably, /tmp is isolated and all directories
except for /var/lib/maddy and /run/maddy are read-only. You will need to modify
systemd unit if your command needs more privileges.
Command output should consist of zero or more lines. First one, if non-empty, overrides
destination folder. All other lines contain additional IMAP flags to add
to the message. If command wants to add flags without changing folder - first
line should be empty.
It is valid for command to not write anything to stdout. In this case its
execution will have no effect on delivery.
Output example:
```
Junk
```
In this case, message will be placed in the Junk folder.
```
$Label1
```
In this case, message will be placed in inbox and will have
'$Label1' added.

View file

@ -0,0 +1,181 @@
# SQL-indexed storage
The imapsql module implements database for IMAP index and message
metadata using SQL-based relational database.
Message contents are stored in an "blob store" defined by msg\_store
directive. By default this is a file system directory under /var/lib/maddy.
Supported RDBMS:
- SQLite 3.25.0
- PostgreSQL 9.6 or newer
- CockroachDB 20.1.5 or newer
Account names are required to have the form of a email address (unless configured otherwise)
and are case-insensitive. UTF-8 names are supported with restrictions defined in the
PRECIS UsernameCaseMapped profile.
```
storage.imapsql {
driver sqlite3
dsn imapsql.db
msg_store fs messages/
}
```
imapsql module also can be used as a lookup table.
It returns empty string values for existing usernames. This might be useful
with destination\_in directive e.g. to implement catch-all
addresses (this is a bad idea to do so, this is just an example):
```
destination_in &local_mailboxes {
deliver_to &local_mailboxes
}
destination example.org {
modify {
replace_rcpt regexp ".*" "catchall@example.org"
}
deliver_to &local_mailboxes
}
```
## Arguments
Specify the driver and DSN.
## Configuration directives
**Syntax**: driver _string_ <br>
**Default**: not specified
REQUIRED.
Use a specified driver to communicate with the database. Supported values:
sqlite3, postgres.
Should be specified either via an argument or via this directive.
**Syntax**: dsn _string_ <br>
**Default**: not specified
REQUIRED.
Data Source Name, the driver-specific value that specifies the database to use.
For SQLite3 this is just a file path.
For PostgreSQL: [https://godoc.org/github.com/lib/pq#hdr-Connection\_String\_Parameters](https://godoc.org/github.com/lib/pq#hdr-Connection\_String\_Parameters)
Should be specified either via an argument or via this directive.
**Syntax**: msg\_store _store_ <br>
**Default**: fs messages/
Module to use for message bodies storage.
See "Blob storage" section for what you can use here.
**Syntax**: <br>
compression off <br>
compression _algorithm_ <br>
compression _algorithm_ _level_ <br>
**Default**: off
Apply compression to message contents.
Supported algorithms: lz4, zstd.
**Syntax**: appendlimit _size_ <br>
**Default**: 32M
Don't allow users to add new messages larger than 'size'.
This does not affect messages added when using module as a delivery target.
Use 'max\_message\_size' directive in SMTP endpoint module to restrict it too.
**Syntax**: debug _boolean_ <br>
**Default**: global directive value
Enable verbose logging.
**Syntax**: junk\_mailbox _name_ <br>
**Default**: Junk
The folder to put quarantined messages in. Thishis setting is not used if user
does have a folder with "Junk" special-use attribute.
**Syntax**: disable\_recent _boolean_ <br>
*Default: true
Disable RFC 3501-conforming handling of \Recent flag.
This significantly improves storage performance when SQLite3 or CockroackDB is
used at the cost of confusing clients that use this flag.
**Syntax**: sqlite\_cache\_size _integer_ <br>
**Default**: defined by SQLite
SQLite page cache size. If positive - specifies amount of pages (1 page - 4
KiB) to keep in cache. If negative - specifies approximate upper bound
of cache size in KiB.
**Syntax**: sqlite\_busy\_timeout _integer_ <br>
**Default**: 5000000
SQLite-specific performance tuning option. Amount of milliseconds to wait
before giving up on DB lock.
**Syntax**: imap\_filter { ... } <br>
**Default**: not set
Specifies IMAP filters to apply for messages delivered from SMTP pipeline.
Ex.
```
imap_filter {
command /etc/maddy/sieve.sh {account_name}
}
```
**Syntax:** delivery\_map **table** <br>
**Default:** identity
Use specified table module to map recipient
addresses from incoming messages to mailbox names.
Normalization algorithm specified in delivery\_normalize is appied before
delivery\_map.
**Syntax:** delivery\_normalize _name_ <br>
**Default:** precis\_casefold\_email
Normalization function to apply to email addresses before mapping them
to mailboxes.
See auth\_normalize.
**Syntax**: auth\_map **table** <br>
**Default**: identity
Use specified table module to map authentication
usernames to mailbox names.
Normalization algorithm specified in auth\_normalize is applied before
auth\_map.
**Syntax**: auth\_normalize _name_ <br>
**Default**: precis\_casefold\_email
Normalization function to apply to authentication usernames before mapping
them to mailboxes.
Available options:
- precis\_casefold\_email PRECIS UsernameCaseMapped profile + U-labels form for domain
- precis\_casefold PRECIS UsernameCaseMapped profile for the entire string
- precis\_email PRECIS UsernameCasePreserved profile + U-labels form for domain
- precis PRECIS UsernameCasePreserved profile for the entire string
- casefold Convert to lower case
- noop Nothing
Note: On message delivery, recipient address is unconditionally normalized
using precis\_casefold\_email function.

View file

@ -0,0 +1,6 @@
# Authentication providers
Most authentication providers are also usable as a table
that contains all usernames known to the module. Exceptions are auth.external and
pam as underlying interfaces do not define a way to check credentials
existence.

View file

@ -0,0 +1,38 @@
# Table chaining
The table.chain module allows chaining together multiple table modules
by using value returned by a previous table as an input for the second
table.
Example:
```
table.chain {
step regexp "(.+)(\\+[^+"@]+)?@example.org" "$1@example.org"
step file /etc/maddy/emails
}
```
This will strip +prefix from mailbox before looking it up
in /etc/maddy/emails list.
## Configuration directives
**Syntax**: step _table\_
Adds a table module to the chain. If input value is not in the table
(e.g. file) - return "not exists" error.
**Syntax**: optional\_step _table\_
Same as step but if input value is not in the table - it is passed to the
next step without changes.
Example:
Something like this can be used to map emails to usernames
after translating them via aliases map:
```
table.chain {
optional_step file /etc/maddy/aliases
step regexp "(.+)@(.+)" "$1"
}
```

View file

@ -0,0 +1,12 @@
# Email local part
The module 'table.email\_localpart' extracts and unescaped local ("username") part
of the email address.
E.g.
test@example.org => test
"test @ a"@example.org => test @ a
```
table.email_localpart { }
```

View file

@ -0,0 +1,58 @@
# File
table.file module builds string-string mapping from a text file.
File is reloaded every 15 seconds if there are any changes (detected using
modification time). No changes are applied if file contains syntax errors.
Definition:
```
file <file path>
```
or
```
file {
file <file path>
}
```
Usage example:
```
# Resolve SMTP address aliases using text file mapping.
modify {
replace_rcpt file /etc/maddy/aliases
}
```
## Syntax
Better demonstrated by examples:
```
# Lines starting with # are ignored.
# And so are lines only with whitespace.
# Whenever 'aaa' is looked up, return 'bbb'
aaa: bbb
# Trailing and leading whitespace is ignored.
ccc: ddd
# If there is no colon, the string is translated into ""
# That is, the following line is equivalent to
# aaa:
aaa
# If the same key is used multiple times - table.file will return
# multiple values when queries.
ddd: firstvalue
ddd: secondvalue
# Alternatively, multiple values can be specified
# using a comma. There is no support for escaping
# so you would have to use a different format if you require
# comma-separated values.
ddd: firstvalue, secondvalue
```

View file

@ -0,0 +1,58 @@
# Regexp rewrite table
The 'regexp' module implements table lookups by applying a regular expression
to the key value. If it matches - 'replacement' value is returned with $N
placeholders being replaced with corresponding capture groups from the match.
Otherwise, no value is returned.
The regular expression syntax is the subset of PCRE. See
[https://golang.org/pkg/regexp/syntax](https://golang.org/pkg/regexp/syntax)/ for details.
```
table.regexp <regexp> [replacement] {
full_match yes
case_insensitive yes
expand_placeholders yes
}
```
Note that [replacement] is optional. If it is not included - table.regexp
will return the original string, therefore acting as a regexp match check.
This can be useful in combination in destination\_in for
advanced matching:
```
destination_in regexp ".*-bounce+.*@example.com" {
...
}
```
## Configuration directives
***Syntax***: full\_match _boolean_ <br>
***Default***: yes
Whether to implicitly add start/end anchors to the regular expression.
That is, if 'full\_match' is yes, then the provided regular expression should
match the whole string. With no - partial match is enough.
***Syntax***: case\_insensitive _boolean_ <br>
***Default***: yes
Whether to make matching case-insensitive.
***Syntax***: expand\_placeholders _boolean_ <br>
***Default***: yes
Replace '$name' and '${name}' in the replacement string with contents of
corresponding capture groups from the match.
To insert a literal $ in the output, use $$ in the template.
# Identity table (table.identity)
The module 'identity' is a table module that just returns the key looked up.
```
table.identity { }
```

View file

@ -0,0 +1,110 @@
# SQL query mapping
The table.sql\_query module implements table interface using SQL queries.
Definition:
```
table.sql_query {
driver <driver name>
dsn <data source name>
lookup <lookup query>
# Optional:
init <init query list>
list <list query>
add <add query>
del <del query>
set <set query>
}
```
Usage example:
```
# Resolve SMTP address aliases using PostgreSQL DB.
modify {
replace_rcpt sql_query {
driver postgres
dsn "dbname=maddy user=maddy"
lookup "SELECT alias FROM aliases WHERE address = $1"
}
}
```
## Configuration directives
***Syntax***: driver _driver name_ <br>
***REQUIRED***
Driver to use to access the database.
Supported drivers: postgres, sqlite3 (if compiled with C support)
***Syntax***: dsn _data source name_ <br>
***REQUIRED***
Data Source Name to pass to the driver. For SQLite3 this is just a path to DB
file. For Postgres, see
[https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection\_String\_Parameters](https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection\_String\_Parameters)
***Syntax***: lookup _query_ <br>
***REQUIRED***
SQL query to use to obtain the lookup result.
It will get one named argument containing the lookup key. Use :key
placeholder to access it in SQL. The result row set should contain one row, one
column with the string that will be used as a lookup result. If there are more
rows, they will be ignored. If there are more columns, lookup will fail. If
there are no rows, lookup returns "no results". If there are any error - lookup
will fail.
***Syntax***: init _queries..._ <br>
***Default***: empty
List of queries to execute on initialization. Can be used to configure RDBMS.
Example, to improve SQLite3 performance:
```
table.sql_query {
driver sqlite3
dsn whatever.db
init "PRAGMA journal_mode=WAL" \
"PRAGMA synchronous=NORMAL"
lookup "SELECT alias FROM aliases WHERE address = $1"
}
```
**Syntax:** named\_args _boolean_ <br>
**Default:** yes
Whether to use named parameters binding when executing SQL queries
or not.
Note that maddy's PostgreSQL driver does not support named parameters and
SQLite3 driver has issues handling numbered parameters:
[https://github.com/mattn/go-sqlite3/issues/472](https://github.com/mattn/go-sqlite3/issues/472)
***Syntax:*** add _query_ <br>
***Syntax:*** list _query_ <br>
***Syntax:*** set _query_ <br>
***Syntax:*** del _query_ <br>
***Default:*** none
If queries are set to implement corresponding table operations - table becomes
"mutable" and can be used in contexts that require writable key-value store.
'add' query gets :key, :value named arguments - key and value strings to store.
They should be added to the store. The query **should** not add multiple values
for the same key and **should** fail if the key already exists.
'list' query gets no arguments and should return a column with all keys in
the store.
'set' query gets :key, :value named arguments - key and value and should replace the existing
entry in the database.
'del' query gets :key argument - key and should remove it from the database.
If named\_args is set to "no" - key is passed as the first numbered parameter
($1), value is passed as the second numbered parameter ($2).

View file

@ -0,0 +1,21 @@
# Static table
The 'static' module implements table lookups using key-value pairs in its
configuration.
```
table.static {
entry KEY1 VALUE1
entry KEY2 VALUE2
...
}
```
## Configuration directives
***Syntax***: entry _key_ _value\_
Add an entry to the table.
If the same key is used multiple times, the last one takes effect.

View file

@ -0,0 +1,84 @@
# Local queue
Queue module buffers messages on disk and retries delivery multiple times to
another target to ensure reliable delivery.
It is also responsible for generation of DSN messages
in case of delivery failures.
## Arguments
First argument specifies directory to use for storage.
Relative paths are relative to the StateDirectory.
## Configuration directives
```
target.queue {
target remote
location ...
max_parallelism 16
max_tries 4
bounce {
destination example.org {
deliver_to &local_mailboxes
}
default_destination {
reject
}
}
autogenerated_msg_domain example.org
debug no
}
```
**Syntax**: target _block\_name_ <br>
**Default**: not specified
REQUIRED.
Delivery target to use for final delivery.
**Syntax**: location _directory_ <br>
**Default**: StateDirectory/configuration\_block\_name
File system directory to use to store queued messages.
Relative paths are relative to the StateDirectory.
**Syntax**: max\_parallelism _integer_ <br>
**Default**: 16
Start up to _integer_ goroutines for message processing. Basically, this option
limits amount of messages tried to be delivered concurrently.
**Syntax**: max\_tries _integer_ <br>
**Default**: 20
Attempt delivery up to _integer_ times. Note that no more attempts will be done
is permanent error occured during previous attempt.
Delay before the next attempt will be increased exponentally using the
following formula: 15mins \* 1.2 ^ (n - 1) where n is the attempt number.
This gives you approximately the following sequence of delays:
18mins, 21mins, 25mins, 31mins, 37mins, 44mins, 53mins, 64mins, ...
**Syntax**: bounce { ... } <br>
**Default**: not specified
This configuration contains pipeline configuration to be used for generated DSN
(Delivery Status Notifiaction) messages.
If this is block is not present in configuration, DSNs will not be generated.
Note, however, this is not what you want most of the time.
**Syntax**: autogenerated\_msg\_domain _domain_ <br>
**Default**: global directive value
Domain to use in sender address for DSNs. Should be specified too if 'bounce'
block is specified.
**Syntax**: debug _boolean_ <br>
**Default**: no
Enable verbose logging.

View file

@ -0,0 +1,257 @@
# Remote MX delivery
Module that implements message delivery to remote MTAs discovered via DNS MX
records. You probably want to use it with queue module for reliability.
If a message check marks a message as 'quarantined', remote module
will refuse to deliver it.
## Configuration directives
```
target.remote {
hostname mx.example.org
debug no
}
```
**Syntax**: hostname _domain_ <br>
**Default**: global directive value
Hostname to use client greeting (EHLO/HELO command). Some servers require it to
be FQDN, SPF-capable servers check whether it corresponds to the server IP
address, so it is better to set it to a domain that resolves to the server IP.
**Syntax**: limits _config block_ <br>
**Default**: no limits
See ['limits' directive for SMTP endpoint](/reference/endpoints/smtp/#rate-concurrency-limiting).
It works the same except for address domains used for
per-source/per-destination are as observed when message exits the server.
**Syntax**: local\_ip _IP address_ <br>
**Default**: empty
Choose the local IP to bind for outbound SMTP connections.
**Syntax**: force\_ipv4 _boolean_ <br>
**Default**: false
Force resolving outbound SMTP domains to IPv4 addresses. Some server providers
do not offer a way to properly set reverse PTR domains for IPv6 addresses; this
option makes maddy only connect to IPv4 addresses so that its public IPv4 address
is used to connect to that server, and thus reverse PTR checks are made against
its IPv4 address.
Warning: this may break sending outgoing mail to IPv6-only SMTP servers.
**Syntax**: connect\_timeout _duration_ <br>
**Default**: 5m
Timeout for TCP connection establishment.
RFC 5321 recommends 5 minutes for "initial greeting" that includes TCP
handshake. maddy uses two separate timers - one for "dialing" (DNS A/AAAA
lookup + TCP handshake) and another for "initial greeting". This directive
configures the former. The latter is not configurable and is hardcoded to be
5 minutes.
**Syntax**: command\_timeout _duration_ <br>
**Default**: 5m
Timeout for any SMTP command (EHLO, MAIL, RCPT, DATA, etc).
If STARTTLS is used this timeout also applies to TLS handshake.
RFC 5321 recommends 5 minutes for MAIL/RCPT and 3 minutes for
DATA.
**Syntax**: submission\_timeout _duration_ <br>
**Default**: 12m
Time to wait after the entire message is sent (after "final dot").
RFC 5321 recommends 10 minutes.
**Syntax**: debug _boolean_ <br>
**Default**: global directive value
Enable verbose logging.
**Syntax**: requiretls\_override _boolean_ <br>
**Default**: true
Allow local security policy to be disabled using 'TLS-Required' header field in
sent messages. Note that the field has no effect if transparent forwarding is
used, message body should be processed before outbound delivery starts for it
to take effect (e.g. message should be queued using 'queue' module).
**Syntax**: relaxed\_requiretls _boolean_ <br>
**Default**: true
This option disables strict conformance with REQUIRETLS specification and
allows forwarding of messages 'tagged' with REQUIRETLS to MXes that are not
advertising REQUIRETLS support. It is meant to allow REQUIRETLS use without the
need to have support from all servers. It is based on the assumption that
server referenced by MX record is likely the final destination and therefore
there is only need to secure communication towards it and not beyond.
**Syntax**: conn\_reuse\_limit _integer_ <br>
**Default**: 10
Amount of times the same SMTP connection can be used.
Connections are never reused if the previous DATA command failed.
**Syntax**: conn\_max\_idle\_count _integer_ <br>
**Default**: 10
Max. amount of idle connections per recipient domains to keep in cache.
**Syntax**: conn\_max\_idle\_time _integer_ <br>
**Default**: 150 (2.5 min)
Amount of time the idle connection is still considered potentially usable.
## Security policies
**Syntax**: mx\_auth _config block_ <br>
**Default**: no policies
'remote' module implements a number of of schemes and protocols necessary to
ensure security of message delivery. Most of these schemes are concerned with
authentication of recipient server and TLS enforcement.
To enable mechanism, specify its name in the mx\_auth directive block:
```
mx_auth {
dane
mtasts
}
```
Additional configuration is possible if supported by the mechanism by
specifying additional options as a block for the corresponding mechanism.
E.g.
```
mtasts {
cache ram
}
```
If the mx\_auth directive is not specified, no mechanisms are enabled. Note
that, however, this makes outbound SMTP vulnerable to a numberous downgrade
attacks and hence not recommended.
It is possible to share the same set of policies for multiple 'remote' module
instances by defining it at the top-level using 'mx\_auth' module and then
referencing it using standard & syntax:
```
mx_auth outbound_policy {
dane
mtasts {
cache ram
}
}
# ... somewhere else ...
deliver_to remote {
mx_auth &outbound_policy
}
# ... somewhere else ...
deliver_to remote {
mx_auth &outbound_policy
tls_client { ... }
}
```
### MTA-STS
Checks MTA-STS policy of the recipient domain. Provides proper authentication
and TLS enforcement for delivery, but partially vulnerable to persistent active
attacks.
Sets MX level to "mtasts" if the used MX matches MTA-STS policy even if it is
not set to "enforce" mode.
```
mtasts {
cache fs
fs_dir StateDirectory/mtasts_cache
}
```
**Syntax**: cache fs|ram <br>
**Default**: fs
Storage to use for MTA-STS cache. 'fs' is to use a filesystem directory, 'ram'
to store the cache in memory.
It is recommended to use 'fs' since that will not discard the cache (and thus
cause MTA-STS security to disappear) on server restart. However, using the RAM
cache can make sense for high-load configurations with good uptime.
**Syntax**: fs\_dir _directory_ <br>
**Default**: StateDirectory/mtasts\_cache
Filesystem directory to use for policies caching if 'cache' is set to 'fs'.
### DNSSEC
Checks whether MX records are signed. Sets MX level to "dnssec" is they are.
maddy does not validate DNSSEC signatures on its own. Instead it reslies on
the upstream resolver to do so by causing lookup to fail when verification
fails and setting the AD flag for signed and verfified zones. As a safety
measure, if the resolver is not 127.0.0.1 or ::1, the AD flag is ignored.
DNSSEC is currently not supported on Windows and other platforms that do not
have the /etc/resolv.conf file in the standard format.
```
dnssec { }
```
### DANE
Checks TLSA records for the recipient MX. Provides downgrade-resistant TLS
enforcement.
Sets TLS level to "authenticated" if a valid and matching TLSA record uses
DANE-EE or DANE-TA usage type.
See above for notes on DNSSEC. DNSSEC support is required for DANE to work.
```
dane { }
```
### Local policy
Checks effective TLS and MX levels (as set by other policies) against local
configuration.
```
local_policy {
min_tls_level none
min_mx_level none
}
```
Using 'local\_policy off' is equivalent to setting both directives to 'none'.
**Syntax**: min\_tls\_level none|encrypted|authenticated <br>
**Default**: none
Set the minimal TLS security level required for all outbound messages.
See [Security levels](../../seclevels) page for details.
**Syntax**: min\_mx\_level: none|mtasts|dnssec <br>
**Default**: none
Set the minimal MX security level required for all outbound messages.
See [Security levels](../../seclevels) page for details.

View file

@ -0,0 +1,114 @@
# SMTP & LMTP transparent forwarding
Module that implements transparent forwarding of messages over SMTP.
Use in pipeline configuration:
```
deliver_to smtp tcp://127.0.0.1:5353
# or
deliver_to smtp tcp://127.0.0.1:5353 {
# Other settings, see below.
}
```
target.lmtp can be used instead of target.smtp to
use LMTP protocol.
Endpoint addresses use format described in [Configuration files syntax / Address definitions](/reference/config-syntax/#address-definitions).
## Configuration directives
```
target.smtp {
debug no
tls_client {
...
}
attempt_starttls yes
require_tls no
auth off
targets tcp://127.0.0.1:2525
connect_timeout 5m
command_timeout 5m
submission_timeout 12m
}
```
**Syntax**: debug _boolean_ <br>
**Default**: global directive value
Enable verbose logging.
**Syntax**: tls\_client { ... } <br>
**Default**: not specified
Advanced TLS client configuration options. See [TLS configuration / Client](/reference/tls/#client) for details.
**Syntax**: attempt\_starttls _boolean_ <br>
**Default**: yes (no for target.lmtp)
Attempt to use STARTTLS if it is supported by the remote server.
If TLS handshake fails, connection will be retried without STARTTLS
unless 'require\_tls' is also specified.
**Syntax**: require\_tls _boolean_ <br>
**Default**: no
Refuse to pass messages over plain-text connections.
**Syntax**: <br>
auth off <br>
plain _username_ _password_ <br>
forward <br>
external <br>
**Default**: off
Specify the way to authenticate to the remote server.
Valid values:
- off
No authentication.
- plain
Authenticate using specified username-password pair.
**Don't use** this without enforced TLS ('require\_tls').
- forward
Forward credentials specified by the client.
**Don't use** this without enforced TLS ('require\_tls').
- external
Request "external" SASL authentication. This is usually used for
authentication using TLS client certificates. See [TLS configuration / Client](/reference/tls/#client) for details.
**Syntax**: targets _endpoints..._ <br>
**Default:** not specified
REQUIRED.
List of remote server addresses to use. See [Address definitions](/reference/config-syntax/#address-definitions)
for syntax to use. Basically, it is 'tcp://ADDRESS:PORT'
for plain SMTP and 'tls://ADDRESS:PORT' for SMTPS (aka SMTP with Implicit
TLS).
Multiple addresses can be specified, they will be tried in order until connection to
one succeeds (including TLS handshake if TLS is required).
**Syntax**: connect\_timeout _duration_ <br>
**Default**: 5m
Same as for target.remote.
**Syntax**: command\_timeout _duration_ <br>
**Default**: 5m
Same as for target.remote.
**Syntax**: submission\_timeout _duration_ <br>
**Default**: 12m
Same as for target.remote.

225
docs/reference/tls-acme.md Normal file
View file

@ -0,0 +1,225 @@
# Automatic certificate management via ACME
Maddy supports obtaining certificates using ACME protocol.
To use it, create a configuration name for tls.loader.acme
and reference it from endpoints that should use automatically
configured certificates:
```
tls.loader.acme local_tls {
email put-your-email-here@example.org
agreed # indicate your agreement with Let's Encrypt ToS
challenge dns-01
}
smtp tcp://127.0.0.1:25 {
tls &local_tls
...
}
```
You can also use a global `tls` directive to use automatically
obtained certificates for all endpoints:
```
tls &local_tls
```
Currently the only supported challenge is dns-01 one therefore
you also need to configure the DNS provider:
```
tls.loader.acme local_tls {
email maddy-acme@example.org
agreed
challenge dns-01
dns PROVIDER_NAME {
...
}
}
```
See below for supported providers and necessary configuration
for each.
## Configuration directives
```
tls.loader.acme {
debug off
hostname example.maddy.invalid
store_path /var/lib/maddy/acme
ca https://acme-v02.api.letsencrypt.org/directory
test_ca https://acme-staging-v02.api.letsencrypt.org/directory
email test@maddy.invalid
agreed off
challenge dns-01
dns ...
}
```
**Syntax:** debug _boolean_ <br>
**Default:** global directive value
Enable debug logging.
**Syntax:** hostname _str_ <br>
**Default:** global directive value
Domain name to issue certificate for. Required.
**Syntax:** store\_path _path_ <br>
**Default:** state\_dir/acme
Where to store issued certificates and associated metadata.
Currently only filesystem-based store is supported.
**Syntax:** ca _url_ <br>
**Default:** Let's Encrypt production CA
URL of ACME directory to use.
**Syntax:** test\_ca _url_ <br>
**Default:** Let's Encrypt staging CA
URL of ACME directory to use for retries should
primary CA fail.
maddy will keep attempting to issues certificates
using test\_ca until it succeeds then it will switch
back to the one configured via 'ca' option.
This avoids rate limit issues with production CA.
**Syntax:** email _str_ <br>
**Default:** not set
Email to pass while registering an ACME account.
**Syntax:** agreed _boolean_ <br>
**Default:** false
Whether you agreed to ToS of the CA service you are using.
**Syntax:** challenge dns-01 <br>
**Default:** not set
Challenge(s) to use while performing domain verification.
## DNS providers
Support for some providers is not provided by standard builds.
To be able to use these, you need to compile maddy
with "libdns\_PROVIDER" build tag.
E.g.
```
./build.sh -tags 'libdns_googleclouddns'
```
- gandi
```
dns gandi {
api_token "token"
}
```
- digitalocean
```
dns digitalocean {
api_token "..."
}
```
- cloudflare
See [https://github.com/libdns/cloudflare#authenticating](https://github.com/libdns/cloudflare#authenticating)
```
dns cloudflare {
api_token "..."
}
```
- vultr
```
dns vultr {
api_token "..."
}
```
- hetzner
```
dns hetzner {
api_token "..."
}
```
- namecheap
```
dns namecheap {
api_key "..."
api_username "..."
# optional: API endpoint, production one is used if not set.
endpoint "https://api.namecheap.com/xml.response"
# optional: your public IP, discovered using icanhazip.com if not set
client_ip 1.2.3.4
}
```
- googleclouddns (non-default)
```
dns googleclouddns {
project "project_id"
service_account_json "path"
}
```
- route53 (non-default)
```
dns route53 {
secret_access_key "..."
access_key_id "..."
# or use environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
}
```
- leaseweb (non-default)
```
dns leaseweb {
api_key "key"
}
```
- metaname (non-default)
```
dns metaname {
api_key "key"
account_ref "reference"
}
```
- alidns (non-default)
```
dns alidns {
key_id "..."
key_secret "..."
}
```
- namedotcom (non-default)
```
dns namedotcom {
user "..."
token "..."
}
```

155
docs/reference/tls.md Normal file
View file

@ -0,0 +1,155 @@
# TLS configuration
## Server-side
TLS certificates are obtained by modules called "certificate loaders". 'tls' directive
arguments specify name of loader to use and arguments. Due to syntax limitations
advanced configuration for loader should be specified using 'loader' directive, see
below.
```
tls file cert.pem key.pem {
protocols tls1.2 tls1.3
curve X25519
ciphers ...
}
tls {
loader file cert.pem key.pem {
# Options for loader go here.
}
protocols tls1.2 tls1.3
curve X25519
ciphers ...
}
```
### Available certificate loaders
- file
Accepts argument pairs specifying certificate and then key.
E.g. 'tls file certA.pem keyA.pem certB.pem keyB.pem'
If multiple certificates are listed, SNI will be used.
- acme
Automatically obtains a certificate using ACME protocol (Let's Encrypt)
- off
Not really a loader but a special value for tls directive, explicitly disables TLS for
endpoint(s).
## Advanced TLS configuration
**Note: maddy uses secure defaults and TLS handshake is resistant to active downgrade attacks.**
**There is no need to change anything in most cases.**
**Syntax**: <br>
protocols _min\_version_ _max\_version_ <br>
protocols _version_ <br>
**Default**: tls1.0 tls1.3
Minimum/maximum accepted TLS version. If only one value is specified, it will
be the only one usable version.
Valid values are: tls1.0, tls1.1, tls1.2, tls1.3
**Syntax**: ciphers _ciphers..._ <br>
**Default**: Go version-defined set of 'secure ciphers', ordered by hardware
performance
List of supported cipher suites, in preference order. Not used with TLS 1.3.
Valid values:
- RSA-WITH-RC4128-SHA
- RSA-WITH-3DES-EDE-CBC-SHA
- RSA-WITH-AES128-CBC-SHA
- RSA-WITH-AES256-CBC-SHA
- RSA-WITH-AES128-CBC-SHA256
- RSA-WITH-AES128-GCM-SHA256
- RSA-WITH-AES256-GCM-SHA384
- ECDHE-ECDSA-WITH-RC4128-SHA
- ECDHE-ECDSA-WITH-AES128-CBC-SHA
- ECDHE-ECDSA-WITH-AES256-CBC-SHA
- ECDHE-RSA-WITH-RC4128-SHA
- ECDHE-RSA-WITH-3DES-EDE-CBC-SHA
- ECDHE-RSA-WITH-AES128-CBC-SHA
- ECDHE-RSA-WITH-AES256-CBC-SHA
- ECDHE-ECDSA-WITH-AES128-CBC-SHA256
- ECDHE-RSA-WITH-AES128-CBC-SHA256
- ECDHE-RSA-WITH-AES128-GCM-SHA256
- ECDHE-ECDSA-WITH-AES128-GCM-SHA256
- ECDHE-RSA-WITH-AES256-GCM-SHA384
- ECDHE-ECDSA-WITH-AES256-GCM-SHA384
- ECDHE-RSA-WITH-CHACHA20-POLY1305
- ECDHE-ECDSA-WITH-CHACHA20-POLY1305
**Syntax**: curve _curves..._ <br>
**Default**: defined by Go version
The elliptic curves that will be used in an ECDHE handshake, in preference
order.
Valid values: p256, p384, p521, X25519.
## Client
tls\_client directive allows to customize behavior of TLS client implementation,
notably adjusting minimal and maximal TLS versions and allowed cipher suites,
enabling TLS client authentication.
```
tls_client {
protocols tls1.2 tls1.3
ciphers ...
curve X25519
root_ca /etc/ssl/cert.pem
cert /etc/ssl/private/maddy-client.pem
key /etc/ssl/private/maddy-client.pem
}
```
**Syntax**: <br>
protocols _min\_version_ _max\_version_ <br>
protocols _version_ <br>
**Default**: tls1.0 tls1.3
Minimum/maximum accepted TLS version. If only one value is specified, it will
be the only one usable version.
Valid values are: tls1.0, tls1.1, tls1.2, tls1.3
**Syntax**: ciphers _ciphers..._ <br>
**Default**: Go version-defined set of 'secure ciphers', ordered by hardware
performance
List of supported cipher suites, in preference order. Not used with TLS 1.3.
See TLS server configuration for list of supported values.
**Syntax**: curve _curves..._ <br>
**Default**: defined by Go version
The elliptic curves that will be used in an ECDHE handshake, in preference
order.
Valid values: p256, p384, p521, X25519.
**Syntax**: root\_ca _paths..._ <br>
**Default**: system CA pool
List of files with PEM-encoded CA certificates to use when verifying
server certificates.
**Syntax**: <br>
cert _cert\_path_ <br>
key _key\_path_ <br>
**Default**: not specified
Present the specified certificate when server requests a client certificate.
Files should use PEM format. Both directives should be specified.

View file

@ -1,4 +1,4 @@
# Security levels
# Outbound delivery security
maddy implements a number of schemes and protocols for discovery and
enforcement of security features supported by the recipient MTA.
@ -83,8 +83,7 @@ passive attacks.
## maddy security policies
See [**maddy-targets(5)**](../man/\_generated\_maddy-targets.5) page for
description of configuration options available for each policy mechanism
See [Remote MX delivery](/reference/targets/remote/) for description of configuration options available for each policy mechanism
supported by maddy.
[RFC 8461 Section 10.2]: https://www.rfc-editor.org/rfc/rfc8461.html#section-10.2 (SMTP MTA Strict Transport Security - 10.2. Preventing Policy Discovery)

View file

@ -35,8 +35,4 @@ Default mapping of rspamd action -> maddy action is as follows:
- "rewrite subject" => Quarantine
- "soft reject" => Reject with temporary error
- "reject" => Reject with permanent error
- "greylist" => Ignored
This and additional data to pass to rspamd (MTA name, settings ID, etc)
can be configured as described in
[**maddy-checks**(5)](/man/_generated_maddy-filters.5/#rspamd-check-checkrspamd).
- "greylist" => Ignored

View file

@ -21,9 +21,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
package dns
import (
"flag"
maddycli "github.com/foxcpp/maddy/internal/cli"
"github.com/urfave/cli/v2"
)
func init() {
flag.StringVar(&overrideServ, "debug.dnsoverride", "system-default", "replace the DNS resolver address")
maddycli.AddGlobalFlag(&cli.StringFlag{
Name: "debug.dnsoverride",
Usage: "replace the DNS resolver address",
Value: "system-default",
Destination: &overrideServ,
})
}

View file

@ -41,6 +41,10 @@ func (d *Dummy) Lookup(_ context.Context, _ string) (string, bool, error) {
return "", false, nil
}
func (d *Dummy) LookupMulti(_ context.Context, _ string) ([]string, error) {
return []string{""}, nil
}
func (d *Dummy) Name() string {
return "dummy"
}

View file

@ -39,5 +39,5 @@ type IMAPFilter interface {
//
// Errors returned by IMAPFilter will be just logged and will not cause delivery
// to fail.
IMAPFilter(accountName string, meta *MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error)
IMAPFilter(accountName string, rcptTo string, meta *MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error)
}

View file

@ -63,12 +63,12 @@ type ModifierState interface {
RewriteSender(ctx context.Context, mailFrom string) (string, error)
// RewriteRcpt replaces RCPT TO value.
// If no changed are required, this method returns its argument, otherwise
// it returns a new value.
// If no changed are required, this method returns its argument as slice,
// otherwise it returns a slice with 1 or more new values.
//
// MsgPipeline will take of populating MsgMeta.OriginalRcpts. RewriteRcpt
// doesn't do it.
RewriteRcpt(ctx context.Context, rcptTo string) (string, error)
RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error)
// RewriteBody modifies passed Header argument and may optionally
// inspect the passed body buffer to make a decision on new header field values.

17
go.mod
View file

@ -12,23 +12,20 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/digitalocean/godo v1.75.0 // indirect
github.com/emersion/go-imap v1.2.1-0.20220119134953-dcd9ee65c8c7
github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4
github.com/emersion/go-imap-compress v0.0.0-20201103190257-14809af1d1b9
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872
github.com/emersion/go-imap-sortthread v1.2.0
github.com/emersion/go-imap-specialuse v0.0.0-20201101201809-1ab93d3d150e
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69
github.com/emersion/go-message v0.15.0
github.com/emersion/go-milter v0.3.2
github.com/emersion/go-msgauth v0.6.5
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac
github.com/emersion/go-smtp v0.15.1-0.20220119142625-1c322d2783aa
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf
github.com/foxcpp/go-imap-backend-tests v0.0.0-20200617132817-958ea5829771
github.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16
github.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005
github.com/foxcpp/go-imap-namespace v0.0.0-20200722130255-93092adf35f1
github.com/foxcpp/go-imap-sql v0.5.1-0.20210828123943-f74ead8f06cd
github.com/foxcpp/go-mockdns v1.0.0
github.com/foxcpp/go-imap-mess v0.0.0-20220105225909-b3469f4a4315
github.com/foxcpp/go-imap-namespace v0.0.0-20200802091432-08496dd8e0ed
github.com/foxcpp/go-imap-sql v0.5.1-0.20220105233636-946daf36ce81
github.com/foxcpp/go-mockdns v0.0.0-20201212160233-ede2f9158d15
github.com/foxcpp/go-mtasts v0.0.0-20191219193356-62bc3f1f74b8
github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect
github.com/go-ldap/ldap/v3 v3.4.2
@ -61,8 +58,8 @@ require (
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/prometheus/client_golang v1.12.1
github.com/rs/xid v1.3.0 // indirect
github.com/urfave/cli v1.22.5
github.com/vultr/govultr/v2 v2.14.1 // indirect
github.com/urfave/cli/v2 v2.3.0
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0
@ -76,3 +73,5 @@ require (
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
)
replace github.com/emersion/go-imap => github.com/foxcpp/go-imap v1.0.0-beta.1.0.20220105164802-1e767d4cfd62

51
go.sum
View file

@ -108,33 +108,13 @@ github.com/digitalocean/godo v1.75.0 h1:UijUv60I095CqJqGKdjY2RTPnnIa4iFddmq+1wfy
github.com/digitalocean/godo v1.75.0/go.mod h1:GBmu8MkjZmNARE7IXRPmkbbnocNN8+uBm0xbEVw2LCs=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-imap v1.0.0-beta.4.0.20190504114255-4d5af3d05147/go.mod h1:mOPegfAgLVXbhRm1bh2JTX08z2Y3HYmKYpbrKDeAzsQ=
github.com/emersion/go-imap v1.0.0/go.mod h1:MEiDDwwQFcZ+L45Pa68jNGv0qU9kbW+SJzwDpvSfX1s=
github.com/emersion/go-imap v1.0.3/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/emersion/go-imap v1.0.4/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/emersion/go-imap v1.0.5/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/emersion/go-imap v1.2.1-0.20220119134953-dcd9ee65c8c7 h1:2hV3AkHAODve7a+HTzLGZ0k1Rprh+9KbYWl+r06bdMA=
github.com/emersion/go-imap v1.2.1-0.20220119134953-dcd9ee65c8c7/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4 h1:U6LL6F1dYqXpVTwEbXhcfU8hgpNvmjB9xeOAiHN695o=
github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/emersion/go-imap-compress v0.0.0-20201103190257-14809af1d1b9 h1:7dmV11mle4UAQ7lX+Hdzx6akKFg3hVm/UUmQ7t6VgTQ=
github.com/emersion/go-imap-compress v0.0.0-20201103190257-14809af1d1b9/go.mod h1:2Ro1PbmiqYiRe5Ct2sGR5hHaKSVHeRpVZwXx8vyYt98=
github.com/emersion/go-imap-move v0.0.0-20180601155324-5eb20cb834bf/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 h1:HGBfonz0q/zq7y3ew+4oy4emHSvk6bkmV0mdDG3E77M=
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-sortthread v1.1.1-0.20200727121200-18e5fb409fed/go.mod h1:opHOzblOHZKQM1JEy+GPk1217giNLa7kleyWTN06qnc=
github.com/emersion/go-imap-sortthread v1.2.0 h1:EMVEJXPWAhXMWECjR82Rn/tza6MddcvTwGAdTu1vJKU=
github.com/emersion/go-imap-sortthread v1.2.0/go.mod h1:UhenCBupR+vSYRnqJkpjSq84INUCsyAK1MLpogv14pE=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-specialuse v0.0.0-20201101201809-1ab93d3d150e h1:AwVkRMFFUMNu+tx0jchwyoXhS2VClQSzTtByVuzxbsE=
github.com/emersion/go-imap-specialuse v0.0.0-20201101201809-1ab93d3d150e/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69 h1:ltTnRlPdSMMb0a/pg7S31T3g+syYeSS5UVJtiR7ez1Y=
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-message v0.9.1/go.mod h1:m3cK90skCWxm5sIMs1sXxly4Tn9Plvcf6eayHZJ1NzM=
github.com/emersion/go-message v0.10.3/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.14.1/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
@ -143,13 +123,13 @@ github.com/emersion/go-milter v0.3.2 h1:j8hrLXf8PAHFhRHDdBoBKluQveMZYoaK7aRIqvao
github.com/emersion/go-milter v0.3.2/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
github.com/emersion/go-msgauth v0.6.5 h1:UaXBtrjYBM3SWw9BBODeSp0uYtScx3CuIF7/RQfkeWo=
github.com/emersion/go-msgauth v0.6.5/go.mod h1:/jbQISFJgtT12T8akRs20l+wI4HcyN/kWy7VRdHEAmA=
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.1-0.20211006082444-62f6b38f85e4 h1:6unG0XYwWUlJjsbYDI06qcRH5Fe0o978bgL8zNydJ8k=
github.com/emersion/go-smtp v0.15.1-0.20211006082444-62f6b38f85e4/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-smtp v0.15.1-0.20220119142625-1c322d2783aa h1:PZiDDRpQS7p6nFZFt9Pbco8a5FYa5kMhu6V7fTsYE4k=
github.com/emersion/go-smtp v0.15.1-0.20220119142625-1c322d2783aa/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
@ -166,15 +146,21 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf h1:rmBPY5fryjp9zLQYsUmQqqgsYq7qeVfrjtr96Tf9vD8=
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=
github.com/foxcpp/go-imap-backend-tests v0.0.0-20200617132817-958ea5829771 h1:xemWCEhBz86Y8v5YgRBnqf6PdZg+ilVgn2jxWVoLOGo=
github.com/foxcpp/go-imap-backend-tests v0.0.0-20200617132817-958ea5829771/go.mod h1:yUISYv/uXLQ6tQZcds/p/hdcZ5JzrEUifyED2VffWpc=
github.com/foxcpp/go-imap v1.0.0-beta.1.0.20220105164802-1e767d4cfd62 h1:fkQX2NRzBgtR2PC60IXzrhcxr3Gti8zIY9HNhJ43/7w=
github.com/foxcpp/go-imap v1.0.0-beta.1.0.20220105164802-1e767d4cfd62/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16 h1:qheFPDpteiUy7Ym18R68OYenpk85UyKYGkhYTmddSBg=
github.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16/go.mod h1:OPP1AgKxMPo3aHX5pcEZLQhhh5sllFcB8aUN9f6a6X8=
github.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005 h1:pfoFtkTTQ473qStSN79jhCFBWqMQt/3DQ3NGuXvT+50=
github.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005/go.mod h1:34FwxnjC2N+EFs2wMtsHevrZLWRKRuVU8wEcHWKq/nE=
github.com/foxcpp/go-imap-namespace v0.0.0-20200722130255-93092adf35f1 h1:B4zNQ2r4qC7FLn8J8+LWt09fFW0tXddypBPS0+HI50s=
github.com/foxcpp/go-imap-namespace v0.0.0-20200722130255-93092adf35f1/go.mod h1:WJYkFIdxyljR/byiqcYMKUF4iFDej4CaIKe2JJrQxu8=
github.com/foxcpp/go-imap-sql v0.5.1-0.20210828123943-f74ead8f06cd h1:4vpPV74xAqiJD6AVGIK6jucz/Frq70sYRcOAU5FLz6I=
github.com/foxcpp/go-imap-sql v0.5.1-0.20210828123943-f74ead8f06cd/go.mod h1:1dHCAq3XRkYRwTDOtL/vCgvvQ13gLqNt2+nLjL1UHyk=
github.com/foxcpp/go-imap-mess v0.0.0-20220105225909-b3469f4a4315 h1:3MxfvA+zWxl+p5BeQ7pROzigTSHAcalvsExTu1Is41Y=
github.com/foxcpp/go-imap-mess v0.0.0-20220105225909-b3469f4a4315/go.mod h1:S/ELw0SONJ3ffk0ie7TYD6OxoIiyeMI22Fr3kwKUG8s=
github.com/foxcpp/go-imap-namespace v0.0.0-20200802091432-08496dd8e0ed h1:1Jo7geyvunrPSjL6F6D9EcXoNApS5v3LQaro7aUNPnE=
github.com/foxcpp/go-imap-namespace v0.0.0-20200802091432-08496dd8e0ed/go.mod h1:Shows1vmkBWO40ChOClaUe6DUnZrsP1UPAuoWzIUdgQ=
github.com/foxcpp/go-imap-sql v0.5.1-0.20220105233636-946daf36ce81 h1:hd79KlESgagszJqmV+dm36r5NR5NFYCMQ2dWi05gKKs=
github.com/foxcpp/go-imap-sql v0.5.1-0.20220105233636-946daf36ce81/go.mod h1:tl6w1OlN7LLvJOoWTR7bNt0JQE+wPbYr8f3/nJSSlwU=
github.com/foxcpp/go-mockdns v0.0.0-20191216195825-5eabd8dbfe1f/go.mod h1:tPg4cp4nseejPd+UKxtCVQ2hUxNTZ7qQZJa7CLriIeo=
github.com/foxcpp/go-mockdns v0.0.0-20201212160233-ede2f9158d15 h1:nLPjjvpUAODOR6vY/7o0hBIk8iTr19Fvmf8aFx/kC7A=
github.com/foxcpp/go-mockdns v0.0.0-20201212160233-ede2f9158d15/go.mod h1:tPg4cp4nseejPd+UKxtCVQ2hUxNTZ7qQZJa7CLriIeo=
github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
github.com/foxcpp/go-mtasts v0.0.0-20191219193356-62bc3f1f74b8 h1:k8w0iy6GP9oeSZWUH3p2DqZHaXDKZGNs3NZGZMGfQHc=
@ -376,7 +362,6 @@ github.com/libdns/vultr v0.0.0-20211122184636-cd4cb5c12e51/go.mod h1:HXpNE79BzPq
github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/martinlindhe/base36 v0.0.0-20190418230009-7c6542dfbb41/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
@ -388,6 +373,8 @@ github.com/mholt/acmez v1.0.2 h1:C8wsEBIUVi6e0DYoxqCcFuXtwc4AWXL/jgcDjF7mjVo=
github.com/mholt/acmez v1.0.2/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio=
github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
@ -472,9 +459,15 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vultr/govultr/v2 v2.0.0/go.mod h1:2PsEeg+gs3p/Fo5Pw8F9mv+DUBEOlrNZ8GmCTGmhOhs=
github.com/vultr/govultr/v2 v2.9.0 h1:n0a0fGOiHAE07twu1VR3jWTDFDE0+DJ/cIqZqX9IlNw=
github.com/vultr/govultr/v2 v2.9.0/go.mod h1:JjUljQdSZx+MELCAJvZ/JH32bJotmflnsyS0NOjb8Jg=
github.com/vultr/govultr/v2 v2.11.0/go.mod h1:JjUljQdSZx+MELCAJvZ/JH32bJotmflnsyS0NOjb8Jg=
github.com/vultr/govultr/v2 v2.14.1 h1:Z4nd9mXNQ5wd63aw0MZOalFeTkJ8L6Sed3PTqagp4TA=
github.com/vultr/govultr/v2 v2.14.1/go.mod h1:JjUljQdSZx+MELCAJvZ/JH32bJotmflnsyS0NOjb8Jg=

View file

@ -112,11 +112,21 @@ func (a *Auth) ListUsers() ([]string, error) {
}
func (a *Auth) CreateUser(username, password string) error {
return a.CreateUserHash(username, password, HashBcrypt, HashOpts{
BcryptCost: bcrypt.DefaultCost,
})
}
func (a *Auth) CreateUserHash(username, password string, hashAlgo string, opts HashOpts) error {
tbl, ok := a.table.(module.MutableTable)
if !ok {
return fmt.Errorf("%s: table is not mutable, no management functionality available", a.modName)
}
if _, ok := HashCompute[hashAlgo]; !ok {
return fmt.Errorf("%s: unknown hash function: %v", a.modName, hashAlgo)
}
key, err := precis.UsernameCaseMapped.CompareKey(username)
if err != nil {
return fmt.Errorf("%s: create user %s (raw): %w", a.modName, username, err)
@ -130,15 +140,12 @@ func (a *Auth) CreateUser(username, password string) error {
return fmt.Errorf("%s: credentials for %s already exist", a.modName, key)
}
// TODO: Allow to customize hash function.
hash, err := HashCompute[HashBcrypt](HashOpts{
BcryptCost: bcrypt.DefaultCost,
}, password)
hash, err := HashCompute[hashAlgo](opts, password)
if err != nil {
return fmt.Errorf("%s: create user %s: hash generation: %w", a.modName, key, err)
}
if err := tbl.SetKey(key, "bcrypt:"+hash); err != nil {
if err := tbl.SetKey(key, hash+":"+hash); err != nil {
return fmt.Errorf("%s: create user %s: %w", a.modName, key, err)
}
return nil

View file

@ -8,31 +8,33 @@ import (
"github.com/foxcpp/maddy/framework/module"
)
func AuthorizeEmailUse(ctx context.Context, username, addr string, mapping module.Table) (bool, error) {
_, domain, err := address.Split(addr)
if err != nil {
return false, fmt.Errorf("authz: %w", err)
}
var validEmails []string
if multi, ok := mapping.(module.MultiTable); ok {
validEmails, err = multi.LookupMulti(ctx, username)
func AuthorizeEmailUse(ctx context.Context, username string, addrs []string, mapping module.Table) (bool, error) {
for _, addr := range addrs {
_, domain, err := address.Split(addr)
if err != nil {
return false, fmt.Errorf("authz: %w", err)
}
} else {
validEmail, ok, err := mapping.Lookup(ctx, username)
if err != nil {
return false, fmt.Errorf("authz: %w", err)
}
if ok {
validEmails = []string{validEmail}
}
}
for _, ent := range validEmails {
if ent == domain || ent == "*" || ent == addr {
return true, nil
var validEmails []string
if multi, ok := mapping.(module.MultiTable); ok {
validEmails, err = multi.LookupMulti(ctx, username)
if err != nil {
return false, fmt.Errorf("authz: %w", err)
}
} else {
validEmail, ok, err := mapping.Lookup(ctx, username)
if err != nil {
return false, fmt.Errorf("authz: %w", err)
}
if ok {
validEmails = []string{validEmail}
}
}
for _, ent := range validEmails {
if ent == domain || ent == "*" || ent == addr {
return true, nil
}
}
}

View file

@ -162,9 +162,18 @@ func (s *state) authzSender(ctx context.Context, authName, email string) module.
}})
}
var preparedEmail []string
var ok bool
s.log.DebugMsg("normalized names", "from", fromEmailNorm, "auth", authNameNorm)
preparedEmail, ok, err := s.c.emailPrepare.Lookup(ctx, fromEmailNorm)
if emailPrepareMulti, isMulti := s.c.emailPrepare.(module.MultiTable); isMulti {
preparedEmail, err = emailPrepareMulti.LookupMulti(ctx, fromEmailNorm)
ok = len(preparedEmail) > 0
} else {
var preparedEmail_single string
preparedEmail_single, ok, err = s.c.emailPrepare.Lookup(ctx, fromEmailNorm)
preparedEmail = []string{preparedEmail_single}
}
s.log.DebugMsg("authorized emails", "preparedEmail", preparedEmail, "ok", ok)
if err != nil {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
@ -176,7 +185,7 @@ func (s *state) authzSender(ctx context.Context, authName, email string) module.
}})
}
if !ok {
preparedEmail = fromEmailNorm
preparedEmail = []string{fromEmailNorm}
}
ok, err = authz.AuthorizeEmailUse(ctx, authNameNorm, preparedEmail, s.c.userToEmail)

105
internal/cli/app.go Normal file
View file

@ -0,0 +1,105 @@
package maddycli
import (
"flag"
"fmt"
"os"
"strings"
"github.com/foxcpp/maddy/framework/log"
"github.com/urfave/cli/v2"
)
var app *cli.App
func init() {
app = cli.NewApp()
app.Usage = "composable all-in-one mail server"
app.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.
This executable can be used to start the server ('run') and to manipulate
databases used by it (all other subcommands).
`
app.Authors = []*cli.Author{
{
Name: "Maddy Mail Server maintainers & contributors",
Email: "~foxcpp/maddy@lists.sr.ht",
},
}
app.ExitErrHandler = func(c *cli.Context, err error) {
cli.HandleExitCoder(err)
if err != nil {
log.Println(err)
cli.OsExiter(1)
}
}
app.EnableBashCompletion = true
app.Commands = []*cli.Command{
{
Name: "generate-man",
Hidden: true,
Action: func(c *cli.Context) error {
man, err := app.ToMan()
if err != nil {
return err
}
fmt.Println(man)
return nil
},
},
{
Name: "generate-fish-completion",
Hidden: true,
Action: func(c *cli.Context) error {
cp, err := app.ToFishCompletion()
if err != nil {
return err
}
fmt.Println(cp)
return nil
},
},
}
}
func AddGlobalFlag(f cli.Flag) {
app.Flags = append(app.Flags, f)
if err := f.Apply(flag.CommandLine); err != nil {
log.Println("GlobalFlag", f, "could not be mapped to stdlib flag:", err)
}
}
func AddSubcommand(cmd *cli.Command) {
app.Commands = append(app.Commands, cmd)
if cmd.Name == "run" {
// Backward compatibility hack to start the server as just ./maddy
// Needs to be done here so we will register all known flags with
// stdlib before Run is called.
app.Action = func(c *cli.Context) error {
log.Println("WARNING: Starting server not via 'maddy run' is deprecated and will stop working in the next version")
return cmd.Action(c)
}
app.Flags = append(app.Flags, cmd.Flags...)
for _, f := range cmd.Flags {
if err := f.Apply(flag.CommandLine); err != nil {
log.Println("GlobalFlag", f, "could not be mapped to stdlib flag:", err)
}
}
}
}
func Run() {
// Actual entry point is registered in maddy.go.
// Print help when called via maddyctl executable. To be removed
// once backward compatbility hack for 'maddy run' is removed too.
if strings.Contains(os.Args[0], "maddyctl") && len(os.Args) == 1 {
app.Run([]string{os.Args[0], "help"})
return
}
app.Run(os.Args)
}

View file

@ -16,15 +16,14 @@ 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
package ctl
import (
"errors"
"fmt"
appendlimit "github.com/emersion/go-imap-appendlimit"
imapbackend "github.com/emersion/go-imap/backend"
"github.com/foxcpp/maddy/framework/module"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// Copied from go-imap-backend-tests.
@ -32,7 +31,7 @@ import (
// AppendLimitUser is extension for backend.User interface which allows to
// set append limit value for testing and administration purposes.
type AppendLimitUser interface {
appendlimit.User
imapbackend.AppendLimitUser
// SetMessageLimit sets new value for limit.
// nil pointer means no limit.
@ -42,7 +41,7 @@ type AppendLimitUser interface {
func imapAcctAppendlimit(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
return cli.Exit("Error: USERNAME is required", 2)
}
u, err := be.GetIMAPAcct(username)
@ -51,7 +50,7 @@ func imapAcctAppendlimit(be module.Storage, ctx *cli.Context) error {
}
userAL, ok := u.(AppendLimitUser)
if !ok {
return errors.New("Error: module.Storage does not support per-user append limit")
return cli.Exit("Error: module.Storage does not support per-user append limit", 2)
}
if ctx.IsSet("value") {

View file

@ -16,20 +16,61 @@ 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
package ctl
import (
"errors"
"fmt"
"os"
"strings"
"github.com/foxcpp/maddy/cmd/maddyctl/clitools"
"github.com/foxcpp/maddy/internal/auth/pass_table"
"github.com/urfave/cli"
maddycli "github.com/foxcpp/maddy/internal/cli"
clitools2 "github.com/foxcpp/maddy/internal/cli/clitools"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/bcrypt"
)
func init() {
maddycli.AddSubcommand(
&cli.Command{
Name: "hash",
Usage: "Generate password hashes for use with pass_table",
Action: hashCommand,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Use `PASSWORD instead of reading password from stdin\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!",
},
&cli.StringFlag{
Name: "hash",
Usage: "Use specified hash algorithm",
Value: "bcrypt",
},
&cli.IntFlag{
Name: "bcrypt-cost",
Usage: "Specify bcrypt cost value",
Value: bcrypt.DefaultCost,
},
&cli.IntFlag{
Name: "argon2-time",
Usage: "Time factor for Argon2id",
Value: 3,
},
&cli.IntFlag{
Name: "argon2-memory",
Usage: "Memory in KiB to use for Argon2id",
Value: 1024,
},
&cli.IntFlag{
Name: "argon2-threads",
Usage: "Threads to use for Argon2id",
Value: 1,
},
},
})
}
func hashCommand(ctx *cli.Context) error {
hashFunc := ctx.String("hash")
if hashFunc == "" {
@ -43,7 +84,7 @@ func hashCommand(ctx *cli.Context) error {
funcs = append(funcs, k)
}
return fmt.Errorf("Error: Unknown hash function, available: %s", strings.Join(funcs, ", "))
return cli.Exit(fmt.Sprintf("Error: Unknown hash function, available: %s", strings.Join(funcs, ", ")), 2)
}
opts := pass_table.HashOpts{
@ -54,10 +95,10 @@ func hashCommand(ctx *cli.Context) error {
}
if ctx.IsSet("bcrypt-cost") {
if ctx.Int("bcrypt-cost") > bcrypt.MaxCost {
return errors.New("Error: too big bcrypt cost")
return cli.Exit("Error: too big bcrypt cost", 2)
}
if ctx.Int("bcrypt-cost") < bcrypt.MinCost {
return errors.New("Error: too small bcrypt cost")
return cli.Exit("Error: too small bcrypt cost", 2)
}
opts.BcryptCost = ctx.Int("bcrypt-cost")
}
@ -76,14 +117,17 @@ func hashCommand(ctx *cli.Context) error {
pass = ctx.String("password")
} else {
var err error
pass, err = clitools.ReadPassword("Password")
pass, err = clitools2.ReadPassword("Password")
if err != nil {
return err
}
}
if pass == "" {
fmt.Fprintln(os.Stderr, "WARNING: This is the hash of empty string")
fmt.Fprintln(os.Stderr, "WARNING: This is the hash of an empty string")
}
if strings.TrimSpace(pass) != pass {
fmt.Fprintln(os.Stderr, "WARNING: There is leading/trailing whitespace in the string")
}
hash, err := hashCompute(opts, pass)

868
internal/cli/ctl/imap.go Normal file
View file

@ -0,0 +1,868 @@
/*
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 ctl
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/emersion/go-imap"
imapsql "github.com/foxcpp/go-imap-sql"
"github.com/foxcpp/maddy/framework/module"
maddycli "github.com/foxcpp/maddy/internal/cli"
clitools2 "github.com/foxcpp/maddy/internal/cli/clitools"
"github.com/urfave/cli/v2"
)
func init() {
maddycli.AddSubcommand(
&cli.Command{
Name: "imap-mboxes",
Usage: "IMAP mailboxes (folders) management",
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "Show mailboxes of user",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "subscribed",
Aliases: []string{"s"},
Usage: "List only subscribed mailboxes",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return mboxesList(be, ctx)
},
},
{
Name: "create",
Usage: "Create mailbox",
ArgsUsage: "USERNAME NAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.StringFlag{
Name: "special",
Usage: "Set SPECIAL-USE attribute on mailbox; valid values: archive, drafts, junk, sent, trash",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return mboxesCreate(be, ctx)
},
},
{
Name: "remove",
Usage: "Remove mailbox",
Description: "WARNING: All contents of mailbox will be irrecoverably lost.",
ArgsUsage: "USERNAME MAILBOX",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return mboxesRemove(be, ctx)
},
},
{
Name: "rename",
Usage: "Rename mailbox",
Description: "Rename may cause unexpected failures on client-side so be careful.",
ArgsUsage: "USERNAME OLDNAME NEWNAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return mboxesRename(be, ctx)
},
},
},
})
maddycli.AddSubcommand(&cli.Command{
Name: "imap-msgs",
Usage: "IMAP messages management",
Subcommands: []*cli.Command{
{
Name: "add",
Usage: "Add message to mailbox",
ArgsUsage: "USERNAME MAILBOX",
Description: "Reads message body (with headers) from stdin. Prints UID of created message on success.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.StringSliceFlag{
Name: "flag",
Aliases: []string{"f"},
Usage: "Add flag to message. Can be specified multiple times",
},
&cli.TimestampFlag{
Layout: time.RFC3339,
Name: "date",
Aliases: []string{"d"},
Usage: "Set internal date value to specified one in ISO 8601 format (2006-01-02T15:04:05Z07:00)",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsAdd(be, ctx)
},
},
{
Name: "add-flags",
Usage: "Add flags to messages",
ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...",
Description: "Add flags to all messages matched by SEQ.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "uid",
Aliases: []string{"u"},
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsFlags(be, ctx)
},
},
{
Name: "rem-flags",
Usage: "Remove flags from messages",
ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...",
Description: "Remove flags from all messages matched by SEQ.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "uid",
Aliases: []string{"u"},
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsFlags(be, ctx)
},
},
{
Name: "set-flags",
Usage: "Set flags on messages",
ArgsUsage: "USERNAME MAILBOX SEQ FLAGS...",
Description: "Set flags on all messages matched by SEQ.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "uid",
Aliases: []string{"u"},
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsFlags(be, ctx)
},
},
{
Name: "remove",
Usage: "Remove messages from mailbox",
ArgsUsage: "USERNAME MAILBOX SEQSET",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "uid,u",
Aliases: []string{"u"},
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsRemove(be, ctx)
},
},
{
Name: "copy",
Usage: "Copy messages between mailboxes",
Description: "Note: You can't copy between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.",
ArgsUsage: "USERNAME SRCMAILBOX SEQSET TGTMAILBOX",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "uid",
Aliases: []string{"u"},
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsCopy(be, ctx)
},
},
{
Name: "move",
Usage: "Move messages between mailboxes",
Description: "Note: You can't move between mailboxes of different users. APPENDLIMIT of target mailbox is not enforced.",
ArgsUsage: "USERNAME SRCMAILBOX SEQSET TGTMAILBOX",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "uid",
Aliases: []string{"u"},
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsMove(be, ctx)
},
},
{
Name: "list",
Usage: "List messages in mailbox",
Description: "If SEQSET is specified - only show messages that match it.",
ArgsUsage: "USERNAME MAILBOX [SEQSET]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "uid",
Aliases: []string{"u"},
Usage: "Use UIDs for SEQSET instead of sequence numbers",
},
&cli.BoolFlag{
Name: "full,f",
Aliases: []string{"f"},
Usage: "Show entire envelope and all server meta-data",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsList(be, ctx)
},
},
{
Name: "dump",
Usage: "Dump message body",
Description: "If passed SEQ matches multiple messages - they will be joined.",
ArgsUsage: "USERNAME MAILBOX SEQ",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "uid",
Aliases: []string{"u"},
Usage: "Use UIDs for SEQ instead of sequence numbers",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return msgsDump(be, ctx)
},
},
},
})
}
func FormatAddress(addr *imap.Address) string {
return fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName)
}
func FormatAddressList(addrs []*imap.Address) string {
res := make([]string, 0, len(addrs))
for _, addr := range addrs {
res = append(res, FormatAddress(addr))
}
return strings.Join(res, ", ")
}
func mboxesList(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
mboxes, err := u.ListMailboxes(ctx.Bool("subscribed,s"))
if err != nil {
return err
}
if len(mboxes) == 0 && !ctx.Bool("quiet") {
fmt.Fprintln(os.Stderr, "No mailboxes.")
}
for _, info := range mboxes {
if len(info.Attributes) != 0 {
fmt.Print(info.Name, "\t", info.Attributes, "\n")
} else {
fmt.Println(info.Name)
}
}
return nil
}
func mboxesCreate(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
name := ctx.Args().Get(1)
if name == "" {
return cli.Exit("Error: NAME is required", 2)
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
if ctx.IsSet("special") {
attr := "\\" + strings.Title(ctx.String("special"))
suu, ok := u.(SpecialUseUser)
if !ok {
return cli.Exit("Error: storage backend does not support SPECIAL-USE IMAP extension", 2)
}
return suu.CreateMailboxSpecial(name, attr)
}
return u.CreateMailbox(name)
}
func mboxesRemove(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
name := ctx.Args().Get(1)
if name == "" {
return cli.Exit("Error: NAME is required", 2)
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
if !ctx.Bool("yes") {
status, err := u.Status(name, []imap.StatusItem{imap.StatusMessages})
if err != nil {
return err
}
if status.Messages != 0 {
fmt.Fprintf(os.Stderr, "Mailbox %s contains %d messages.\n", name, status.Messages)
}
if !clitools2.Confirmation("Are you sure you want to delete that mailbox?", false) {
return errors.New("Cancelled")
}
}
return u.DeleteMailbox(name)
}
func mboxesRename(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
oldName := ctx.Args().Get(1)
if oldName == "" {
return cli.Exit("Error: OLDNAME is required", 2)
}
newName := ctx.Args().Get(2)
if newName == "" {
return cli.Exit("Error: NEWNAME is required", 2)
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
return u.RenameMailbox(oldName, newName)
}
func msgsAdd(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
name := ctx.Args().Get(1)
if name == "" {
return cli.Exit("Error: MAILBOX is required", 2)
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
flags := ctx.StringSlice("flag")
if flags == nil {
flags = []string{}
}
date := time.Now()
if ctx.IsSet("date") {
date = *ctx.Timestamp("date")
}
buf := bytes.Buffer{}
if _, err := io.Copy(&buf, os.Stdin); err != nil {
return err
}
if buf.Len() == 0 {
return cli.Exit("Error: Empty message, refusing to continue", 2)
}
status, err := u.Status(name, []imap.StatusItem{imap.StatusUidNext})
if err != nil {
return err
}
if err := u.CreateMessage(name, flags, date, &buf, nil); err != nil {
return err
}
// TODO: Use APPENDUID
fmt.Println(status.UidNext)
return nil
}
func msgsRemove(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
name := ctx.Args().Get(1)
if name == "" {
return cli.Exit("Error: MAILBOX is required", 2)
}
seqset := ctx.Args().Get(2)
if seqset == "" {
return cli.Exit("Error: SEQSET is required", 2)
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
_, mbox, err := u.GetMailbox(name, true, nil)
if err != nil {
return err
}
if !ctx.Bool("yes") {
if !clitools2.Confirmation("Are you sure you want to delete these messages?", false) {
return errors.New("Cancelled")
}
}
mboxB := mbox.(*imapsql.Mailbox)
return mboxB.DelMessages(ctx.Bool("uid"), seq)
}
func msgsCopy(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
srcName := ctx.Args().Get(1)
if srcName == "" {
return cli.Exit("Error: SRCMAILBOX is required", 2)
}
seqset := ctx.Args().Get(2)
if seqset == "" {
return cli.Exit("Error: SEQSET is required", 2)
}
tgtName := ctx.Args().Get(3)
if tgtName == "" {
return cli.Exit("Error: TGTMAILBOX is required", 2)
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
_, srcMbox, err := u.GetMailbox(srcName, true, nil)
if err != nil {
return err
}
return srcMbox.CopyMessages(ctx.Bool("uid"), seq, tgtName)
}
func msgsMove(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
srcName := ctx.Args().Get(1)
if srcName == "" {
return cli.Exit("Error: SRCMAILBOX is required", 2)
}
seqset := ctx.Args().Get(2)
if seqset == "" {
return cli.Exit("Error: SEQSET is required", 2)
}
tgtName := ctx.Args().Get(3)
if tgtName == "" {
return cli.Exit("Error: TGTMAILBOX is required", 2)
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
_, srcMbox, err := u.GetMailbox(srcName, true, nil)
if err != nil {
return err
}
moveMbox := srcMbox.(*imapsql.Mailbox)
return moveMbox.MoveMessages(ctx.Bool("uid"), seq, tgtName)
}
func msgsList(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
mboxName := ctx.Args().Get(1)
if mboxName == "" {
return cli.Exit("Error: MAILBOX is required", 2)
}
seqset := ctx.Args().Get(2)
if seqset == "" {
seqset = "1:*"
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
_, mbox, err := u.GetMailbox(mboxName, true, nil)
if err != nil {
return err
}
ch := make(chan *imap.Message, 10)
go func() {
err = mbox.ListMessages(ctx.Bool("uid"), seq, []imap.FetchItem{imap.FetchEnvelope, imap.FetchInternalDate, imap.FetchRFC822Size, imap.FetchFlags, imap.FetchUid}, ch)
}()
for msg := range ch {
if !ctx.Bool("full") {
fmt.Printf("UID %d: %s - %s\n %v, %v\n\n", msg.Uid, FormatAddressList(msg.Envelope.From), msg.Envelope.Subject, msg.Flags, msg.Envelope.Date)
continue
}
fmt.Println("- Server meta-data:")
fmt.Println("UID:", msg.Uid)
fmt.Println("Sequence number:", msg.SeqNum)
fmt.Println("Flags:", msg.Flags)
fmt.Println("Body size:", msg.Size)
fmt.Println("Internal date:", msg.InternalDate.Unix(), msg.InternalDate)
fmt.Println("- Envelope:")
if len(msg.Envelope.From) != 0 {
fmt.Println("From:", FormatAddressList(msg.Envelope.From))
}
if len(msg.Envelope.To) != 0 {
fmt.Println("To:", FormatAddressList(msg.Envelope.To))
}
if len(msg.Envelope.Cc) != 0 {
fmt.Println("CC:", FormatAddressList(msg.Envelope.Cc))
}
if len(msg.Envelope.Bcc) != 0 {
fmt.Println("BCC:", FormatAddressList(msg.Envelope.Bcc))
}
if msg.Envelope.InReplyTo != "" {
fmt.Println("In-Reply-To:", msg.Envelope.InReplyTo)
}
if msg.Envelope.MessageId != "" {
fmt.Println("Message-Id:", msg.Envelope.MessageId)
}
if !msg.Envelope.Date.IsZero() {
fmt.Println("Date:", msg.Envelope.Date.Unix(), msg.Envelope.Date)
}
if msg.Envelope.Subject != "" {
fmt.Println("Subject:", msg.Envelope.Subject)
}
fmt.Println()
}
return err
}
func msgsDump(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
mboxName := ctx.Args().Get(1)
if mboxName == "" {
return cli.Exit("Error: MAILBOX is required", 2)
}
seqset := ctx.Args().Get(2)
if seqset == "" {
seqset = "*"
}
seq, err := imap.ParseSeqSet(seqset)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
_, mbox, err := u.GetMailbox(mboxName, true, nil)
if err != nil {
return err
}
ch := make(chan *imap.Message, 10)
go func() {
err = mbox.ListMessages(ctx.Bool("uid"), seq, []imap.FetchItem{imap.FetchRFC822}, ch)
}()
for msg := range ch {
for _, v := range msg.Body {
if _, err := io.Copy(os.Stdout, v); err != nil {
return err
}
}
}
return err
}
func msgsFlags(be module.Storage, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
name := ctx.Args().Get(1)
if name == "" {
return cli.Exit("Error: MAILBOX is required", 2)
}
seqStr := ctx.Args().Get(2)
if seqStr == "" {
return cli.Exit("Error: SEQ is required", 2)
}
seq, err := imap.ParseSeqSet(seqStr)
if err != nil {
return err
}
u, err := be.GetIMAPAcct(username)
if err != nil {
return err
}
_, mbox, err := u.GetMailbox(name, true, nil)
if err != nil {
return err
}
flags := ctx.Args().Slice()[3:]
if len(flags) == 0 {
return cli.Exit("Error: at least once FLAG is required", 2)
}
var op imap.FlagsOp
switch ctx.Command.Name {
case "add-flags":
op = imap.AddFlags
case "rem-flags":
op = imap.RemoveFlags
case "set-flags":
op = imap.SetFlags
default:
panic("unknown command: " + ctx.Command.Name)
}
return mbox.UpdateMessagesFlags(ctx.IsSet("uid"), seq, op, true, flags)
}

View file

@ -0,0 +1,292 @@
/*
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 ctl
import (
"errors"
"fmt"
"os"
"github.com/emersion/go-imap"
"github.com/foxcpp/maddy/framework/module"
maddycli "github.com/foxcpp/maddy/internal/cli"
clitools2 "github.com/foxcpp/maddy/internal/cli/clitools"
"github.com/urfave/cli/v2"
)
func init() {
maddycli.AddSubcommand(
&cli.Command{
Name: "imap-acct",
Usage: "IMAP storage accounts management",
Description: `These subcommands can be used to list/create/delete IMAP storage
accounts for any storage backend supported by maddy.
The corresponding storage backend should be configured in maddy.conf and be
defined in a top-level configuration block. By default, the name of that
block should be local_mailboxes but this can be changed using --cfg-block
flag for subcommands.
Note that in default configuration it is not enough to create an IMAP storage
account to grant server access. Additionally, user credentials should
be created using 'creds' subcommand.
`,
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List storage accounts",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return imapAcctList(be, ctx)
},
},
{
Name: "create",
Usage: "Create IMAP storage account",
Description: `In addition to account creation, this command
creates a set of default folder (mailboxes) with special-use attribute set.`,
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.StringFlag{
Name: "sent-name",
Usage: "Name of special mailbox for sent messages, use empty string to not create any",
Value: "Sent",
},
&cli.StringFlag{
Name: "trash-name",
Usage: "Name of special mailbox for trash, use empty string to not create any",
Value: "Trash",
},
&cli.StringFlag{
Name: "junk-name",
Usage: "Name of special mailbox for 'junk' (spam), use empty string to not create any",
Value: "Junk",
},
&cli.StringFlag{
Name: "drafts-name",
Usage: "Name of special mailbox for drafts, use empty string to not create any",
Value: "Drafts",
},
&cli.StringFlag{
Name: "archive-name",
Usage: "Name of special mailbox for archive, use empty string to not create any",
Value: "Archive",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return imapAcctCreate(be, ctx)
},
},
{
Name: "remove",
Usage: "Delete IMAP storage account",
Description: `If IMAP connections are open and using the specified account,
messages access will be killed off immediately though connection will remain open. No cache
or other buffering takes effect.`,
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return imapAcctRemove(be, ctx)
},
},
{
Name: "appendlimit",
Usage: "Query or set accounts's APPENDLIMIT value",
Description: `APPENDLIMIT value determines the size of a message that
can be saved into a mailbox using IMAP APPEND command. This does not affect the size
of messages that can be delivered to the mailbox from non-IMAP sources (e.g. SMTP).
Global APPENDLIMIT value set via server configuration takes precedence over
per-account values configured using this command.
APPENDLIMIT value (either global or per-account) cannot be larger than
4 GiB due to IMAP protocol limitations.
`,
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_mailboxes",
},
&cli.IntFlag{
Name: "value",
Aliases: []string{"v"},
Usage: "Set APPENDLIMIT to specified value (in bytes)",
},
},
Action: func(ctx *cli.Context) error {
be, err := openStorage(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return imapAcctAppendlimit(be, ctx)
},
},
},
})
}
type SpecialUseUser interface {
CreateMailboxSpecial(name, specialUseAttr string) error
}
func imapAcctList(be module.Storage, ctx *cli.Context) error {
mbe, ok := be.(module.ManageableStorage)
if !ok {
return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2)
}
list, err := mbe.ListIMAPAccts()
if err != nil {
return err
}
if len(list) == 0 && !ctx.Bool("quiet") {
fmt.Fprintln(os.Stderr, "No users.")
}
for _, user := range list {
fmt.Println(user)
}
return nil
}
func imapAcctCreate(be module.Storage, ctx *cli.Context) error {
mbe, ok := be.(module.ManageableStorage)
if !ok {
return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2)
}
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
if err := mbe.CreateIMAPAcct(username); err != nil {
return err
}
act, err := mbe.GetIMAPAcct(username)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
suu, ok := act.(SpecialUseUser)
if !ok {
fmt.Fprintf(os.Stderr, "Note: Storage backend does not support SPECIAL-USE IMAP extension")
}
createMbox := func(name, specialUseAttr string) error {
if suu == nil {
return act.CreateMailbox(name)
}
return suu.CreateMailboxSpecial(name, specialUseAttr)
}
if name := ctx.String("sent-name"); name != "" {
if err := createMbox(name, imap.SentAttr); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create sent folder: %v", err)
}
}
if name := ctx.String("trash-name"); name != "" {
if err := createMbox(name, imap.TrashAttr); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create trash folder: %v", err)
}
}
if name := ctx.String("junk-name"); name != "" {
if err := createMbox(name, imap.JunkAttr); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create junk folder: %v", err)
}
}
if name := ctx.String("drafts-name"); name != "" {
if err := createMbox(name, imap.DraftsAttr); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create drafts folder: %v", err)
}
}
if name := ctx.String("archive-name"); name != "" {
if err := createMbox(name, imap.ArchiveAttr); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create archive folder: %v", err)
}
}
return nil
}
func imapAcctRemove(be module.Storage, ctx *cli.Context) error {
mbe, ok := be.(module.ManageableStorage)
if !ok {
return cli.Exit("Error: storage backend does not support accounts management using maddyctl", 2)
}
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
if !ctx.Bool("yes") {
if !clitools2.Confirmation("Are you sure you want to delete this user account?", false) {
return errors.New("Cancelled")
}
}
return mbe.DeleteIMAPAcct(username)
}

View file

@ -0,0 +1,133 @@
/*
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 ctl
import (
"errors"
"fmt"
"io"
"os"
"github.com/foxcpp/maddy"
parser "github.com/foxcpp/maddy/framework/cfgparser"
"github.com/foxcpp/maddy/framework/config"
"github.com/foxcpp/maddy/framework/hooks"
"github.com/foxcpp/maddy/framework/module"
"github.com/foxcpp/maddy/internal/updatepipe"
"github.com/urfave/cli/v2"
)
func closeIfNeeded(i interface{}) {
if c, ok := i.(io.Closer); ok {
c.Close()
}
}
func getCfgBlockModule(ctx *cli.Context) (map[string]interface{}, *maddy.ModInfo, error) {
cfgPath := ctx.String("config")
if cfgPath == "" {
return nil, nil, cli.Exit("Error: config is required", 2)
}
cfgFile, err := os.Open(cfgPath)
if err != nil {
return nil, nil, cli.Exit(fmt.Sprintf("Error: failed to open config: %v", err), 2)
}
defer cfgFile.Close()
cfgNodes, err := parser.Read(cfgFile, cfgFile.Name())
if err != nil {
return nil, nil, cli.Exit(fmt.Sprintf("Error: failed to parse config: %v", err), 2)
}
globals, cfgNodes, err := maddy.ReadGlobals(cfgNodes)
if err != nil {
return nil, nil, err
}
if err := maddy.InitDirs(); err != nil {
return nil, nil, err
}
module.NoRun = true
_, mods, err := maddy.RegisterModules(globals, cfgNodes)
if err != nil {
return nil, nil, err
}
defer hooks.RunHooks(hooks.EventShutdown)
cfgBlock := ctx.String("cfg-block")
if cfgBlock == "" {
return nil, nil, cli.Exit("Error: cfg-block is required", 2)
}
var mod maddy.ModInfo
for _, m := range mods {
if m.Instance.InstanceName() == cfgBlock {
mod = m
break
}
}
if mod.Instance == nil {
return nil, nil, cli.Exit(fmt.Sprintf("Error: unknown configuration block: %s", cfgBlock), 2)
}
return globals, &mod, nil
}
func openStorage(ctx *cli.Context) (module.Storage, error) {
globals, mod, err := getCfgBlockModule(ctx)
if err != nil {
return nil, err
}
storage, ok := mod.Instance.(module.Storage)
if !ok {
return nil, cli.Exit(fmt.Sprintf("Error: configuration block %s is not an IMAP storage", ctx.String("cfg-block")), 2)
}
if err := mod.Instance.Init(config.NewMap(globals, mod.Cfg)); err != nil {
return nil, fmt.Errorf("Error: module initialization failed: %w", err)
}
if updStore, ok := mod.Instance.(updatepipe.Backend); ok {
if err := updStore.EnableUpdatePipe(updatepipe.ModePush); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "Failed to initialize update pipe, do not remove messages from mailboxes open by clients: %v\n", err)
}
} else {
fmt.Fprintf(os.Stderr, "No update pipe support, do not remove messages from mailboxes open by clients\n")
}
return storage, nil
}
func openUserDB(ctx *cli.Context) (module.PlainUserDB, error) {
globals, mod, err := getCfgBlockModule(ctx)
if err != nil {
return nil, err
}
userDB, ok := mod.Instance.(module.PlainUserDB)
if !ok {
return nil, cli.Exit(fmt.Sprintf("Error: configuration block %s is not a local credentials store", ctx.String("cfg-block")), 2)
}
if err := mod.Instance.Init(config.NewMap(globals, mod.Cfg)); err != nil {
return nil, fmt.Errorf("Error: module initialization failed: %w", err)
}
return userDB, nil
}

246
internal/cli/ctl/users.go Normal file
View file

@ -0,0 +1,246 @@
/*
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 ctl
import (
"errors"
"fmt"
"os"
"strings"
"github.com/foxcpp/maddy/framework/module"
"github.com/foxcpp/maddy/internal/auth/pass_table"
maddycli "github.com/foxcpp/maddy/internal/cli"
clitools2 "github.com/foxcpp/maddy/internal/cli/clitools"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/bcrypt"
)
func init() {
maddycli.AddSubcommand(
&cli.Command{
Name: "creds",
Usage: "Local credentials management",
Description: `These commands manipulate credential databases used by
maddy mail server.
Corresponding credential database should be defined in maddy.conf as
a top-level config block. By default the block name should be local_authdb (
can be changed using --cfg-block argument for subcommands).
Note that it is not enough to create user credentials in order to grant
IMAP access - IMAP account should be also created using 'imap-acct create' subcommand.
`,
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List created credentials",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_authdb",
},
},
Action: func(ctx *cli.Context) error {
be, err := openUserDB(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return usersList(be, ctx)
},
},
{
Name: "create",
Usage: "Create user account",
Description: `Reads password from stdin.
If configuration block uses auth.pass_table, then hash algorithm can be configured
using command flags. Otherwise, these options cannot be used.
`,
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_authdb",
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Use `PASSWORD instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!",
},
&cli.StringFlag{
Name: "hash",
Usage: "Use specified hash algorithm. Valid values: " + strings.Join(pass_table.Hashes, ", "),
Value: "bcrypt",
},
&cli.IntFlag{
Name: "bcrypt-cost",
Usage: "Specify bcrypt cost value",
Value: bcrypt.DefaultCost,
},
},
Action: func(ctx *cli.Context) error {
be, err := openUserDB(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return usersCreate(be, ctx)
},
},
{
Name: "remove",
Usage: "Delete user account",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_authdb",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "Don't ask for confirmation",
},
},
Action: func(ctx *cli.Context) error {
be, err := openUserDB(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return usersRemove(be, ctx)
},
},
{
Name: "password",
Usage: "Change account password",
Description: "Reads password from stdin",
ArgsUsage: "USERNAME",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "cfg-block",
Usage: "Module configuration block to use",
EnvVars: []string{"MADDY_CFGBLOCK"},
Value: "local_authdb",
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Use `PASSWORD` instead of reading password from stdin.\n\t\tWARNING: Provided only for debugging convenience. Don't leave your passwords in shell history!",
},
},
Action: func(ctx *cli.Context) error {
be, err := openUserDB(ctx)
if err != nil {
return err
}
defer closeIfNeeded(be)
return usersPassword(be, ctx)
},
},
},
})
}
func usersList(be module.PlainUserDB, ctx *cli.Context) error {
list, err := be.ListUsers()
if err != nil {
return err
}
if len(list) == 0 && !ctx.Bool("quiet") {
fmt.Fprintln(os.Stderr, "No users.")
}
for _, user := range list {
fmt.Println(user)
}
return nil
}
func usersCreate(be module.PlainUserDB, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return cli.Exit("Error: USERNAME is required", 2)
}
var pass string
if ctx.IsSet("password") {
pass = ctx.String("password")
} else {
var err error
pass, err = clitools2.ReadPassword("Enter password for new user")
if err != nil {
return err
}
}
if be, ok := be.(*pass_table.Auth); ok {
return be.CreateUserHash(username, pass, ctx.String("hash"), pass_table.HashOpts{
BcryptCost: ctx.Int("bcrypt-cost"),
})
} else if ctx.IsSet("hash") || ctx.IsSet("bcrypt-cost") {
return cli.Exit("Error: --hash cannot be used with non-pass_table credentials DB", 2)
} else {
return be.CreateUser(username, pass)
}
}
func usersRemove(be module.PlainUserDB, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
if !ctx.Bool("yes") {
if !clitools2.Confirmation("Are you sure you want to delete this user account?", false) {
return errors.New("Cancelled")
}
}
return be.DeleteUser(username)
}
func usersPassword(be module.PlainUserDB, ctx *cli.Context) error {
username := ctx.Args().First()
if username == "" {
return errors.New("Error: USERNAME is required")
}
var pass string
if ctx.IsSet("password") {
pass = ctx.String("password")
} else {
var err error
pass, err = clitools2.ReadPassword("Enter new password")
if err != nil {
return err
}
}
return be.SetUserPassword(username, pass)
}

View file

@ -27,12 +27,8 @@ import (
"sync"
"github.com/emersion/go-imap"
appendlimit "github.com/emersion/go-imap-appendlimit"
compress "github.com/emersion/go-imap-compress"
move "github.com/emersion/go-imap-move"
sortthread "github.com/emersion/go-imap-sortthread"
specialuse "github.com/emersion/go-imap-specialuse"
unselect "github.com/emersion/go-imap-unselect"
imapbackend "github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-message"
@ -40,7 +36,6 @@ import (
"github.com/emersion/go-sasl"
i18nlevel "github.com/foxcpp/go-imap-i18nlevel"
namespace "github.com/foxcpp/go-imap-namespace"
"github.com/foxcpp/go-imap-sql/children"
"github.com/foxcpp/maddy/framework/config"
modconfig "github.com/foxcpp/maddy/framework/config/module"
tls2 "github.com/foxcpp/maddy/framework/config/tls"
@ -56,7 +51,6 @@ type Endpoint struct {
listeners []net.Listener
Store module.Storage
updater imapbackend.BackendUpdater
tlsConfig *tls.Config
listenersWg sync.WaitGroup
@ -97,24 +91,12 @@ func (endp *Endpoint) Init(cfg *config.Map) error {
return err
}
var ok bool
endp.updater, ok = endp.Store.(imapbackend.BackendUpdater)
if !ok {
return fmt.Errorf("imap: storage module %T does not implement imapbackend.BackendUpdater", endp.Store)
}
if updBe, ok := endp.Store.(updatepipe.Backend); ok {
if err := updBe.EnableUpdatePipe(updatepipe.ModeReplicate); err != nil {
endp.Log.Error("failed to initialize updates pipe", err)
}
}
// Call Updates once at start, some storage backends initialize update
// channel lazily and may not generate updates at all unless it is called.
if endp.updater.Updates() == nil {
return fmt.Errorf("imap: failed to init backend: nil update channel")
}
addresses := make([]config.Endpoint, 0, len(endp.addrs))
for _, addr := range endp.addrs {
saddr, err := config.ParseEndpoint(addr)
@ -193,10 +175,6 @@ func (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {
return nil
}
func (endp *Endpoint) Updates() <-chan imapbackend.Update {
return endp.updater.Updates()
}
func (endp *Endpoint) Name() string {
return "imap"
}
@ -237,10 +215,6 @@ func (endp *Endpoint) Login(connInfo *imap.ConnInfo, username, password string)
return endp.Store.GetOrCreateIMAPAcct(username)
}
func (endp *Endpoint) EnableChildrenExt() bool {
return endp.Store.(children.Backend).EnableChildrenExt()
}
func (endp *Endpoint) I18NLevel() int {
be, ok := endp.Store.(i18nlevel.Backend)
if !ok {
@ -253,14 +227,6 @@ func (endp *Endpoint) enableExtensions() error {
exts := endp.Store.IMAPExtensions()
for _, ext := range exts {
switch ext {
case "APPENDLIMIT":
endp.serv.Enable(appendlimit.NewExtension())
case "CHILDREN":
endp.serv.Enable(children.NewExtension())
case "MOVE":
endp.serv.Enable(move.NewExtension())
case "SPECIAL-USE":
endp.serv.Enable(specialuse.NewExtension())
case "I18NLEVEL=1", "I18NLEVEL=2":
endp.serv.Enable(i18nlevel.NewExtension())
case "SORT":
@ -272,7 +238,6 @@ func (endp *Endpoint) enableExtensions() error {
}
endp.serv.Enable(compress.NewExtension())
endp.serv.Enable(unselect.NewExtension())
endp.serv.Enable(namespace.NewExtension())
return nil

View file

@ -48,8 +48,8 @@ type Check struct {
cmdArgs []string
}
func (c *Check) IMAPFilter(accountName string, msgMeta *module.MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error) {
cmd, args := c.expandCommand(msgMeta, accountName)
func (c *Check) IMAPFilter(accountName string, rcptTo string, msgMeta *module.MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error) {
cmd, args := c.expandCommand(msgMeta, accountName, rcptTo, hdr)
var buf bytes.Buffer
_ = textproto.WriteHeader(&buf, hdr)
@ -95,7 +95,7 @@ func (c *Check) Init(cfg *config.Map) error {
return err
}
func (c *Check) expandCommand(msgMeta *module.MsgMetadata, accountName string) (string, []string) {
func (c *Check) expandCommand(msgMeta *module.MsgMetadata, accountName string, rcptTo string, hdr textproto.Header) (string, []string) {
expArgs := make([]string, len(c.cmdArgs))
for i, arg := range c.cmdArgs {
@ -136,6 +136,16 @@ func (c *Check) expandCommand(msgMeta *module.MsgMetadata, accountName string) (
return msgMeta.ID
case "{sender}":
return msgMeta.OriginalFrom
case "{rcpt_to}":
return rcptTo
case "{original_rcpt_to}":
oldestOriginalRcpt := rcptTo
for originalRcpt, ok := rcptTo, true; ok; originalRcpt, ok = msgMeta.OriginalRcpts[originalRcpt] {
oldestOriginalRcpt = originalRcpt
}
return oldestOriginalRcpt
case "{subject}":
return hdr.Get("Subject")
case "{account_name}":
return accountName
}

View file

@ -44,7 +44,7 @@ func NewGroup(_, instName string, _, _ []string) (module.Module, error) {
}, nil
}
func (g *Group) IMAPFilter(accountName string, meta *module.MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error) {
func (g *Group) IMAPFilter(accountName string, rcptTo string, meta *module.MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error) {
if g == nil {
return "", nil, nil
}
@ -53,7 +53,7 @@ func (g *Group) IMAPFilter(accountName string, meta *module.MsgMetadata, hdr tex
finalFlags = make([]string, 0, len(g.Filters))
)
for _, f := range g.Filters {
folder, flags, err := f.IMAPFilter(accountName, meta, hdr, body)
folder, flags, err := f.IMAPFilter(accountName, rcptTo, meta, hdr, body)
if err != nil {
g.log.Error("IMAP filter failed", err)
continue

View file

@ -283,8 +283,8 @@ func (s *state) RewriteSender(ctx context.Context, mailFrom string) (string, err
return mailFrom, nil
}
func (s state) RewriteRcpt(ctx context.Context, rcptTo string) (string, error) {
return rcptTo, nil
func (s state) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {
return []string{rcptTo}, nil
}
func (s *state) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {

View file

@ -91,15 +91,22 @@ func (gs groupState) RewriteSender(ctx context.Context, mailFrom string) (string
return mailFrom, nil
}
func (gs groupState) RewriteRcpt(ctx context.Context, rcptTo string) (string, error) {
func (gs groupState) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {
var err error
var result = []string{rcptTo}
for _, state := range gs.states {
rcptTo, err = state.RewriteRcpt(ctx, rcptTo)
if err != nil {
return "", err
var intermediateResult = []string{}
for _, partResult := range result {
var partResult_multi []string
partResult_multi, err = state.RewriteRcpt(ctx, partResult)
if err != nil {
return []string{""}, err
}
intermediateResult = append(intermediateResult, partResult_multi...)
}
result = intermediateResult
}
return rcptTo, nil
return result, nil
}
func (gs groupState) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {

View file

@ -43,7 +43,7 @@ type replaceAddr struct {
replaceSender bool
replaceRcpt bool
table module.Table
table module.MultiTable
}
func NewReplaceAddr(modName, instName string, _, inlineArgs []string) (module.Module, error) {
@ -76,16 +76,20 @@ func (r replaceAddr) ModStateForMsg(ctx context.Context, msgMeta *module.MsgMeta
func (r replaceAddr) RewriteSender(ctx context.Context, mailFrom string) (string, error) {
if r.replaceSender {
return r.rewrite(ctx, mailFrom)
results, err := r.rewrite(ctx, mailFrom)
if err != nil {
return mailFrom, err
}
mailFrom = results[0]
}
return mailFrom, nil
}
func (r replaceAddr) RewriteRcpt(ctx context.Context, rcptTo string) (string, error) {
func (r replaceAddr) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {
if r.replaceRcpt {
return r.rewrite(ctx, rcptTo)
}
return rcptTo, nil
return []string{rcptTo}, nil
}
func (r replaceAddr) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {
@ -96,47 +100,54 @@ func (r replaceAddr) Close() error {
return nil
}
func (r replaceAddr) rewrite(ctx context.Context, val string) (string, error) {
func (r replaceAddr) rewrite(ctx context.Context, val string) ([]string, error) {
normAddr, err := address.ForLookup(val)
if err != nil {
return val, fmt.Errorf("malformed address: %v", err)
return []string{val}, fmt.Errorf("malformed address: %v", err)
}
replacement, ok, err := r.table.Lookup(ctx, normAddr)
replacements, err := r.table.LookupMulti(ctx, normAddr)
if err != nil {
return val, err
return []string{val}, err
}
if ok {
if !address.Valid(replacement) {
return "", fmt.Errorf("refusing to replace recipient with the invalid address %s", replacement)
if len(replacements) > 0 {
for _, replacement := range replacements {
if !address.Valid(replacement) {
return []string{""}, fmt.Errorf("refusing to replace recipient with the invalid address %s", replacement)
}
}
return replacement, nil
return replacements, nil
}
mbox, domain, err := address.Split(normAddr)
if err != nil {
// If we have malformed address here, something is really wrong, but let's
// ignore it silently then anyway.
return val, nil
return []string{val}, nil
}
// mbox is already normalized, since it is a part of address.ForLookup
// result.
replacement, ok, err = r.table.Lookup(ctx, mbox)
replacements, err = r.table.LookupMulti(ctx, mbox)
if err != nil {
return val, err
return []string{val}, err
}
if ok {
if strings.Contains(replacement, "@") && !strings.HasPrefix(replacement, `"`) && !strings.HasSuffix(replacement, `"`) {
if !address.Valid(replacement) {
return "", fmt.Errorf("refusing to replace recipient with invalid address %s", replacement)
if len(replacements) > 0 {
var results = make([]string, len(replacements))
for i, replacement := range replacements {
if strings.Contains(replacement, "@") && !strings.HasPrefix(replacement, `"`) && !strings.HasSuffix(replacement, `"`) {
if !address.Valid(replacement) {
return []string{""}, fmt.Errorf("refusing to replace recipient with invalid address %s", replacement)
}
results[i] = replacement
} else {
results[i] = replacement + "@" + domain
}
return replacement, nil
}
return replacement + "@" + domain, nil
return results, nil
}
return val, nil
return []string{val}, nil
}
func init() {

View file

@ -20,6 +20,7 @@ package modify
import (
"context"
"reflect"
"testing"
"github.com/foxcpp/maddy/framework/config"
@ -27,7 +28,7 @@ import (
)
func testReplaceAddr(t *testing.T, modName string) {
test := func(addr, expected string, aliases map[string]string) {
test := func(addr string, expectedMulti []string, aliases map[string][]string) {
t.Helper()
mod, err := NewReplaceAddr(modName, "", nil, []string{"dummy"})
@ -38,60 +39,71 @@ func testReplaceAddr(t *testing.T, modName string) {
if err := m.Init(config.NewMap(nil, config.Node{})); err != nil {
t.Fatal(err)
}
m.table = testutils.Table{M: aliases}
m.table = testutils.MultiTable{M: aliases}
var actual string
var actualMulti []string
if modName == "modify.replace_sender" {
var actual string
actual, err = m.RewriteSender(context.Background(), addr)
if err != nil {
t.Fatal(err)
}
actualMulti = []string{actual}
}
if modName == "modify.replace_rcpt" {
actual, err = m.RewriteRcpt(context.Background(), addr)
actualMulti, err = m.RewriteRcpt(context.Background(), addr)
if err != nil {
t.Fatal(err)
}
}
if actual != expected {
t.Errorf("want %s, got %s", expected, actual)
if !reflect.DeepEqual(actualMulti, expectedMulti) {
t.Errorf("want %s, got %s", expectedMulti, actualMulti)
}
}
test("test@example.org", "test@example.org", nil)
test("postmaster", "postmaster", nil)
test("test@example.com", "test@example.org",
map[string]string{"test@example.com": "test@example.org"})
test(`"\"test @ test\""@example.com`, "test@example.org",
map[string]string{`"\"test @ test\""@example.com`: "test@example.org"})
test(`test@example.com`, `"\"test @ test\""@example.org`,
map[string]string{`test@example.com`: `"\"test @ test\""@example.org`})
test(`"\"test @ test\""@example.com`, `"\"b @ b\""@example.com`,
map[string]string{`"\"test @ test\""`: `"\"b @ b\""`})
test("TeSt@eXAMple.com", "test@example.org",
map[string]string{"test@example.com": "test@example.org"})
test("test@example.com", "test2@example.com",
map[string]string{"test": "test2"})
test("test@example.com", "test2@example.org",
map[string]string{"test": "test2@example.org"})
test("postmaster", "test2@example.org",
map[string]string{"postmaster": "test2@example.org"})
test("TeSt@examPLE.com", "test2@example.com",
map[string]string{"test": "test2"})
test("test@example.com", "test3@example.com",
map[string]string{
"test@example.com": "test3@example.com",
"test": "test2",
test("test@example.org", []string{"test@example.org"}, nil)
test("postmaster", []string{"postmaster"}, nil)
test("test@example.com", []string{"test@example.org"},
map[string][]string{"test@example.com": []string{"test@example.org"}})
test(`"\"test @ test\""@example.com`, []string{"test@example.org"},
map[string][]string{`"\"test @ test\""@example.com`: []string{"test@example.org"}})
test(`test@example.com`, []string{`"\"test @ test\""@example.org`},
map[string][]string{`test@example.com`: []string{`"\"test @ test\""@example.org`}})
test(`"\"test @ test\""@example.com`, []string{`"\"b @ b\""@example.com`},
map[string][]string{`"\"test @ test\""`: []string{`"\"b @ b\""`}})
test("TeSt@eXAMple.com", []string{"test@example.org"},
map[string][]string{"test@example.com": []string{"test@example.org"}})
test("test@example.com", []string{"test2@example.com"},
map[string][]string{"test": []string{"test2"}})
test("test@example.com", []string{"test2@example.org"},
map[string][]string{"test": []string{"test2@example.org"}})
test("postmaster", []string{"test2@example.org"},
map[string][]string{"postmaster": []string{"test2@example.org"}})
test("TeSt@examPLE.com", []string{"test2@example.com"},
map[string][]string{"test": []string{"test2"}})
test("test@example.com", []string{"test3@example.com"},
map[string][]string{
"test@example.com": []string{"test3@example.com"},
"test": []string{"test2"},
})
test("rcpt@E\u0301.example.com", "rcpt@foo.example.com",
map[string]string{
"rcpt@\u00E9.example.com": "rcpt@foo.example.com",
test("rcpt@E\u0301.example.com", []string{"rcpt@foo.example.com"},
map[string][]string{
"rcpt@\u00E9.example.com": []string{"rcpt@foo.example.com"},
})
test("E\u0301@foo.example.com", "rcpt@foo.example.com",
map[string]string{
"\u00E9@foo.example.com": "rcpt@foo.example.com",
test("E\u0301@foo.example.com", []string{"rcpt@foo.example.com"},
map[string][]string{
"\u00E9@foo.example.com": []string{"rcpt@foo.example.com"},
})
if modName == "modify.replace_rcpt" {
//multiple aliases
test("test@example.com", []string{"test@example.org", "test@example.net"},
map[string][]string{"test@example.com": []string{"test@example.org", "test@example.net"}})
test("test@example.com", []string{"1@example.com", "2@example.com", "3@example.com"},
map[string][]string{"test@example.com": []string{"1@example.com", "2@example.com", "3@example.com"}})
}
}
func TestReplaceAddr_RewriteSender(t *testing.T) {

View file

@ -79,8 +79,8 @@ func TestMsgPipeline_BodyNonAtomic_ModifiedRcpt(t *testing.T) {
Modifiers: []module.Modifier{
testutils.Modifier{
InstName: "test_modifier",
RcptTo: map[string]string{
"tester@example.org": "tester-alias@example.org",
RcptTo: map[string][]string{
"tester@example.org": []string{"tester-alias@example.org"},
},
},
},

View file

@ -234,9 +234,9 @@ func TestMsgPipeline_RcptModifier(t *testing.T) {
target := testutils.Target{}
mod := testutils.Modifier{
InstName: "test_modifier",
RcptTo: map[string]string{
"rcpt1@example.com": "rcpt1-alias@example.com",
"rcpt2@example.com": "rcpt2-alias@example.com",
RcptTo: map[string][]string{
"rcpt1@example.com": []string{"rcpt1-alias@example.com"},
"rcpt2@example.com": []string{"rcpt2-alias@example.com"},
},
}
d := MsgPipeline{
@ -272,9 +272,9 @@ func TestMsgPipeline_RcptModifier_OriginalRcpt(t *testing.T) {
target := testutils.Target{}
mod := testutils.Modifier{
InstName: "test_modifier",
RcptTo: map[string]string{
"rcpt1@example.com": "rcpt1-alias@example.com",
"rcpt2@example.com": "rcpt2-alias@example.com",
RcptTo: map[string][]string{
"rcpt1@example.com": []string{"rcpt1-alias@example.com"},
"rcpt2@example.com": []string{"rcpt2-alias@example.com"},
},
}
d := MsgPipeline{
@ -318,15 +318,15 @@ func TestMsgPipeline_RcptModifier_OriginalRcpt_Multiple(t *testing.T) {
target := testutils.Target{}
mod1, mod2 := testutils.Modifier{
InstName: "first_modifier",
RcptTo: map[string]string{
"rcpt1@example.com": "rcpt1-alias@example.com",
"rcpt2@example.com": "rcpt2-alias@example.com",
RcptTo: map[string][]string{
"rcpt1@example.com": []string{"rcpt1-alias@example.com"},
"rcpt2@example.com": []string{"rcpt2-alias@example.com"},
},
}, testutils.Modifier{
InstName: "second_modifier",
RcptTo: map[string]string{
"rcpt1-alias@example.com": "rcpt1-alias2@example.com",
"rcpt2@example.com": "wtf@example.com",
RcptTo: map[string][]string{
"rcpt1-alias@example.com": []string{"rcpt1-alias2@example.com"},
"rcpt2@example.com": []string{"wtf@example.com"},
},
}
d := MsgPipeline{
@ -373,15 +373,15 @@ func TestMsgPipeline_RcptModifier_Multiple(t *testing.T) {
target := testutils.Target{}
mod1, mod2 := testutils.Modifier{
InstName: "first_modifier",
RcptTo: map[string]string{
"rcpt1@example.com": "rcpt1-alias@example.com",
"rcpt2@example.com": "rcpt2-alias@example.com",
RcptTo: map[string][]string{
"rcpt1@example.com": []string{"rcpt1-alias@example.com"},
"rcpt2@example.com": []string{"rcpt2-alias@example.com"},
},
}, testutils.Modifier{
InstName: "second_modifier",
RcptTo: map[string]string{
"rcpt1-alias@example.com": "rcpt1-alias2@example.com",
"rcpt2@example.com": "wtf@example.com",
RcptTo: map[string][]string{
"rcpt1-alias@example.com": []string{"rcpt1-alias2@example.com"},
"rcpt2@example.com": []string{"wtf@example.com"},
},
}
d := MsgPipeline{
@ -417,15 +417,15 @@ func TestMsgPipeline_RcptModifier_PreDispatch(t *testing.T) {
target := testutils.Target{}
mod1, mod2 := testutils.Modifier{
InstName: "first_modifier",
RcptTo: map[string]string{
"rcpt1@example.com": "rcpt1-alias@example.com",
"rcpt2@example.com": "rcpt2-alias@example.com",
RcptTo: map[string][]string{
"rcpt1@example.com": []string{"rcpt1-alias@example.com"},
"rcpt2@example.com": []string{"rcpt2-alias@example.com"},
},
}, testutils.Modifier{
InstName: "second_modifier",
RcptTo: map[string]string{
"rcpt1-alias@example.com": "rcpt1-alias2@example.com",
"rcpt2@example.com": "wtf@example.com",
RcptTo: map[string][]string{
"rcpt1-alias@example.com": []string{"rcpt1-alias2@example.com"},
"rcpt2@example.com": []string{"wtf@example.com"},
},
}
d := MsgPipeline{
@ -469,9 +469,9 @@ func TestMsgPipeline_RcptModifier_PostDispatch(t *testing.T) {
target := testutils.Target{}
mod := testutils.Modifier{
InstName: "test_modifier",
RcptTo: map[string]string{
"rcpt1@example.com": "rcpt1@example.org",
"rcpt2@example.com": "rcpt2@example.org",
RcptTo: map[string][]string{
"rcpt1@example.com": []string{"rcpt1@example.org"},
"rcpt2@example.com": []string{"rcpt2@example.org"},
},
}
d := MsgPipeline{

View file

@ -292,75 +292,85 @@ func (dd *msgpipelineDelivery) AddRcpt(ctx context.Context, to string) error {
return err
}
dd.log.Debugln("global rcpt modifiers:", to, "=>", newTo)
to = newTo
newTo, err = dd.sourceModifiersState.RewriteRcpt(ctx, to)
if err != nil {
return err
resultTo := newTo
newTo = []string{}
for _, to = range resultTo {
var tempTo []string
tempTo, err = dd.sourceModifiersState.RewriteRcpt(ctx, to)
if err != nil {
return err
}
newTo = append(newTo, tempTo...)
}
dd.log.Debugln("per-source rcpt modifiers:", to, "=>", newTo)
to = newTo
resultTo = newTo
wrapErr := func(err error) error {
return exterrors.WithFields(err, map[string]interface{}{
"effective_rcpt": to,
})
}
rcptBlock, err := dd.rcptBlockForAddr(ctx, to)
if err != nil {
return wrapErr(err)
}
if rcptBlock.rejectErr != nil {
return wrapErr(rcptBlock.rejectErr)
}
if err := dd.checkRunner.checkRcpt(ctx, rcptBlock.checks, to); err != nil {
return wrapErr(err)
}
rcptModifiersState, err := dd.getRcptModifiers(ctx, rcptBlock, to)
if err != nil {
return wrapErr(err)
}
newTo, err = rcptModifiersState.RewriteRcpt(ctx, to)
if err != nil {
rcptModifiersState.Close()
return wrapErr(err)
}
dd.log.Debugln("per-rcpt modifiers:", to, "=>", newTo)
to = newTo
wrapErr = func(err error) error {
return exterrors.WithFields(err, map[string]interface{}{
"effective_rcpt": to,
})
}
if originalTo != to {
dd.msgMeta.OriginalRcpts[to] = originalTo
}
for _, tgt := range rcptBlock.targets {
// Do not wrap errors coming from nested pipeline target delivery since
// that pipeline itself will insert effective_rcpt field and could do
// its own rewriting - we do not want to hide it from the admin in
// error messages.
wrapErr := wrapErr
if _, ok := tgt.(*MsgPipeline); ok {
wrapErr = func(err error) error { return err }
for _, to = range resultTo {
wrapErr := func(err error) error {
return exterrors.WithFields(err, map[string]interface{}{
"effective_rcpt": to,
})
}
delivery, err := dd.getDelivery(ctx, tgt)
rcptBlock, err := dd.rcptBlockForAddr(ctx, to)
if err != nil {
return wrapErr(err)
}
if err := delivery.AddRcpt(ctx, to); err != nil {
if rcptBlock.rejectErr != nil {
return wrapErr(rcptBlock.rejectErr)
}
if err := dd.checkRunner.checkRcpt(ctx, rcptBlock.checks, to); err != nil {
return wrapErr(err)
}
delivery.recipients = append(delivery.recipients, originalTo)
rcptModifiersState, err := dd.getRcptModifiers(ctx, rcptBlock, to)
if err != nil {
return wrapErr(err)
}
newTo, err = rcptModifiersState.RewriteRcpt(ctx, to)
if err != nil {
rcptModifiersState.Close()
return wrapErr(err)
}
dd.log.Debugln("per-rcpt modifiers:", to, "=>", newTo)
for _, to = range newTo {
wrapErr = func(err error) error {
return exterrors.WithFields(err, map[string]interface{}{
"effective_rcpt": to,
})
}
if originalTo != to {
dd.msgMeta.OriginalRcpts[to] = originalTo
}
for _, tgt := range rcptBlock.targets {
// Do not wrap errors coming from nested pipeline target delivery since
// that pipeline itself will insert effective_rcpt field and could do
// its own rewriting - we do not want to hide it from the admin in
// error messages.
wrapErr := wrapErr
if _, ok := tgt.(*MsgPipeline); ok {
wrapErr = func(err error) error { return err }
}
delivery, err := dd.getDelivery(ctx, tgt)
if err != nil {
return wrapErr(err)
}
if err := delivery.AddRcpt(ctx, to); err != nil {
return wrapErr(err)
}
delivery.recipients = append(delivery.recipients, originalTo)
}
}
}
return nil

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