├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature-request.md ├── SECURITY.md ├── releases.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .mkdocs.yml ├── .version ├── COPYING ├── Dockerfile ├── HACKING.md ├── README.md ├── build.sh ├── cmd ├── README.md ├── maddy-pam-helper │ ├── README.md │ ├── maddy.conf │ ├── main.c │ ├── main.go │ ├── pam.c │ └── pam.h ├── maddy-shadow-helper │ ├── README.md │ └── main.go └── maddy │ └── main.go ├── config.go ├── contrib ├── README.md └── kubernetes │ └── chart │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── files │ ├── aliases │ └── maddy.conf │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ ├── pvc.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── directories.go ├── directories_docker.go ├── dist ├── README.md ├── apparmor │ └── dev.foxcpp.maddy ├── fail2ban │ ├── filter.d │ │ ├── maddy-auth.conf │ │ └── maddy-dictonary-attack.conf │ └── jail.d │ │ ├── maddy-auth.conf │ │ └── maddy-dictonary-attack.conf ├── install.sh ├── logrotate.d │ └── maddy ├── systemd │ ├── maddy.service │ └── maddy@.service └── vim │ ├── ftdetect │ └── maddy-conf.vim │ ├── ftplugin │ └── maddy-conf.vim │ └── syntax │ └── maddy-conf.vim ├── docs ├── docker.md ├── faq.md ├── index.md ├── internals │ ├── quirks.md │ ├── specifications.md │ ├── sqlite.md │ └── unicode.md ├── man │ ├── .gitignore │ ├── README.md │ ├── maddy.1.scd │ └── prepare_md.py ├── multiple-domains.md ├── reference │ ├── auth │ │ ├── dovecot_sasl.md │ │ ├── external.md │ │ ├── ldap.md │ │ ├── netauth.md │ │ ├── pam.md │ │ ├── pass_table.md │ │ ├── plain_separate.md │ │ └── shadow.md │ ├── blob │ │ ├── fs.md │ │ └── s3.md │ ├── checks │ │ ├── actions.md │ │ ├── authorize_sender.md │ │ ├── command.md │ │ ├── dkim.md │ │ ├── dnsbl.md │ │ ├── milter.md │ │ ├── misc.md │ │ ├── rspamd.md │ │ └── spf.md │ ├── config-syntax.md │ ├── endpoints │ │ ├── imap.md │ │ ├── openmetrics.md │ │ └── smtp.md │ ├── global-config.md │ ├── modifiers │ │ ├── dkim.md │ │ └── envelope.md │ ├── modules.md │ ├── smtp-pipeline.md │ ├── storage │ │ ├── imap-filters.md │ │ └── imapsql.md │ ├── table │ │ ├── auth.md │ │ ├── chain.md │ │ ├── email_localpart.md │ │ ├── email_with_domain.md │ │ ├── file.md │ │ ├── regexp.md │ │ ├── sql_query.md │ │ └── static.md │ ├── targets │ │ ├── queue.md │ │ ├── remote.md │ │ └── smtp.md │ ├── tls-acme.md │ └── tls.md ├── seclevels.md ├── third-party │ ├── dovecot.md │ ├── mailman3.md │ ├── rspamd.md │ └── smtp-servers.md ├── tutorials │ ├── alias-to-remote.md │ ├── building-from-source.md │ ├── pam.md │ └── setting-up.md └── upgrading.md ├── framework ├── address │ ├── doc.go │ ├── norm.go │ ├── norm_test.go │ ├── rfc6531.go │ ├── rfc6531_test.go │ ├── split.go │ ├── split_test.go │ ├── validation.go │ └── validation_test.go ├── buffer │ ├── buffer.go │ ├── bytesreader.go │ ├── file.go │ └── memory.go ├── cfgparser │ ├── env.go │ ├── imports.go │ ├── parse.go │ └── parse_test.go ├── config │ ├── config.go │ ├── directories.go │ ├── endpoint.go │ ├── endpoint_test.go │ ├── lexer │ │ ├── LICENSE.APACHE │ │ ├── README.md │ │ ├── dispenser.go │ │ ├── dispenser_test.go │ │ ├── lexer.go │ │ ├── lexer_test.go │ │ └── parse.go │ ├── map.go │ ├── map_test.go │ ├── module │ │ ├── check_action.go │ │ ├── interfaces.go │ │ └── modconfig.go │ └── tls │ │ ├── client.go │ │ ├── general.go │ │ └── server.go ├── dns │ ├── debugflags.go │ ├── dnssec.go │ ├── dnssec_test.go │ ├── idna.go │ ├── norm.go │ ├── override.go │ └── resolver.go ├── exterrors │ ├── dns.go │ ├── exterrors.go │ ├── fields.go │ ├── smtp.go │ └── temporary.go ├── future │ ├── future.go │ └── future_test.go ├── hooks │ └── hooks.go ├── log │ ├── log.go │ ├── orderedjson.go │ ├── output.go │ ├── syslog.go │ ├── syslog_stub.go │ ├── writer.go │ └── zap.go ├── logparser │ ├── parse.go │ └── parse_test.go └── module │ ├── auth.go │ ├── blob_store.go │ ├── check.go │ ├── delivery_target.go │ ├── dummy.go │ ├── imap_filter.go │ ├── instances.go │ ├── modifier.go │ ├── module.go │ ├── module_specific_data.go │ ├── msgmetadata.go │ ├── mxauth.go │ ├── partial_delivery.go │ ├── registry.go │ ├── storage.go │ ├── table.go │ └── tls_loader.go ├── go.mod ├── go.sum ├── internal ├── README.md ├── auth │ ├── auth.go │ ├── auth_test.go │ ├── dovecot_sasl │ │ └── dovecot_sasl.go │ ├── external │ │ ├── externalauth.go │ │ └── helperauth.go │ ├── ldap │ │ └── ldap.go │ ├── netauth │ │ └── netauth.go │ ├── pam │ │ ├── module.go │ │ ├── pam.c │ │ ├── pam.go │ │ ├── pam.h │ │ └── pam_stub.go │ ├── pass_table │ │ ├── hash.go │ │ ├── table.go │ │ └── table_test.go │ ├── plain_separate │ │ ├── plain_separate.go │ │ └── plain_separate_test.go │ ├── sasl.go │ ├── sasl_test.go │ ├── sasllogin │ │ └── sasllogin.go │ └── shadow │ │ ├── module.go │ │ ├── read.go │ │ ├── shadow.go │ │ └── verify.go ├── authz │ ├── lookup.go │ └── normalization.go ├── check │ ├── authorize_sender │ │ └── authorize_sender.go │ ├── command │ │ └── command.go │ ├── dkim │ │ ├── dkim.go │ │ └── dkim_test.go │ ├── dns │ │ ├── dns.go │ │ └── dns_test.go │ ├── dnsbl │ │ ├── common.go │ │ ├── common_test.go │ │ ├── dnsbl.go │ │ └── dnsbl_test.go │ ├── milter │ │ ├── milter.go │ │ └── milter_test.go │ ├── requiretls │ │ └── requiretls.go │ ├── rspamd │ │ └── rspamd.go │ ├── skeleton.go │ ├── spf │ │ └── spf.go │ └── stateless_check.go ├── cli │ ├── app.go │ ├── clitools │ │ ├── clitools.go │ │ ├── termios.go │ │ └── termios_stub.go │ ├── ctl │ │ ├── appendlimit.go │ │ ├── hash.go │ │ ├── imap.go │ │ ├── imapacct.go │ │ ├── moduleinit.go │ │ └── users.go │ └── extflag.go ├── dmarc │ ├── dmarc.go │ ├── evaluate.go │ ├── evaluate_test.go │ ├── verifier.go │ └── verifier_test.go ├── dsn │ └── dsn.go ├── endpoint │ ├── dovecot_sasld │ │ ├── dovecot_sasl.go │ │ └── mech_info.go │ ├── imap │ │ └── imap.go │ ├── openmetrics │ │ └── om.go │ └── smtp │ │ ├── date.go │ │ ├── metrics.go │ │ ├── session.go │ │ ├── smtp.go │ │ ├── smtp_test.go │ │ ├── smtputf8_test.go │ │ ├── submission.go │ │ └── submission_test.go ├── imap_filter │ ├── command │ │ └── command.go │ └── group.go ├── libdns │ ├── acmedns.go │ ├── alidns.go │ ├── cloudflare.go │ ├── digitalocean.go │ ├── gandi.go │ ├── gcore.go │ ├── googleclouddns.go │ ├── hetzner.go │ ├── leaseweb.go │ ├── metaname.go │ ├── namecheap.go │ ├── namedotcom.go │ ├── provider_module.go │ ├── rfc2136.go │ ├── route53.go │ └── vultr.go ├── limits │ ├── limiters │ │ ├── bucket.go │ │ ├── concurrency.go │ │ ├── limiters.go │ │ ├── multilimit.go │ │ └── rate.go │ └── limits.go ├── modify │ ├── dkim │ │ ├── dkim.go │ │ ├── dkim_test.go │ │ ├── keys.go │ │ └── keys_test.go │ ├── group.go │ ├── replace_addr.go │ └── replace_addr_test.go ├── msgpipeline │ ├── bench_test.go │ ├── bodynonatomic_test.go │ ├── check_group.go │ ├── check_runner.go │ ├── check_test.go │ ├── config.go │ ├── config_test.go │ ├── dmarc_test.go │ ├── metrics.go │ ├── modifier_test.go │ ├── module.go │ ├── msgpipeline.go │ ├── msgpipeline_test.go │ ├── objname.go │ └── regress_test.go ├── proxy_protocol │ └── proxy_protocol.go ├── smtpconn │ ├── pool │ │ └── pool.go │ ├── smtpconn.go │ ├── smtpconn_test.go │ └── smtputf8_test.go ├── storage │ ├── blob │ │ ├── fs │ │ │ ├── fs.go │ │ │ └── fs_test.go │ │ ├── s3 │ │ │ ├── s3.go │ │ │ └── s3_test.go │ │ ├── test_blob.go │ │ └── test_blob_nosqlite.go │ └── imapsql │ │ ├── bench_test.go │ │ ├── delivery.go │ │ ├── external_blob_store.go │ │ ├── imapsql.go │ │ ├── maddyctl.go │ │ ├── modernc_sqlite3.go │ │ ├── no_sqlite3.go │ │ └── sqlite3.go ├── table │ ├── chain.go │ ├── email_localpart.go │ ├── email_with_domain.go │ ├── file.go │ ├── file_test.go │ ├── identity.go │ ├── regexp.go │ ├── sql_query.go │ ├── sql_query_test.go │ ├── sql_table.go │ ├── sqlite3.go │ └── static.go ├── target │ ├── delivery.go │ ├── queue │ │ ├── metrics.go │ │ ├── queue.go │ │ ├── queue_test.go │ │ ├── timewheel.go │ │ └── timewheel_test.go │ ├── received.go │ ├── remote │ │ ├── connect.go │ │ ├── dane.go │ │ ├── dane_delivery_test.go │ │ ├── dane_test.go │ │ ├── debugflags.go │ │ ├── metrics.go │ │ ├── mxauth_test.go │ │ ├── policy_group.go │ │ ├── remote.go │ │ ├── remote_test.go │ │ └── security.go │ ├── skeleton.go │ └── smtp │ │ ├── sasl.go │ │ ├── sasl_test.go │ │ ├── smtp_downstream.go │ │ ├── smtp_downstream_test.go │ │ └── smtputf8_test.go ├── testutils │ ├── bench_delivery.go │ ├── buffer.go │ ├── check.go │ ├── filesystem.go │ ├── logger.go │ ├── modifier.go │ ├── multitable.go │ ├── smtp_server.go │ ├── table.go │ └── target.go ├── tls │ ├── acme │ │ └── acme.go │ ├── file.go │ └── self_signed.go └── updatepipe │ ├── backend.go │ ├── pubsub │ ├── pq.go │ └── pubsub.go │ ├── pubsub_pipe.go │ ├── serialize.go │ ├── unix_pipe.go │ └── update_pipe.go ├── maddy.conf ├── maddy.conf.docker ├── maddy.go ├── maddy_debug.go ├── signal.go ├── signal_nonposix.go ├── systemd.go ├── systemd_nonlinux.go └── tests ├── README.md ├── basic_test.go ├── build_cover.sh ├── conn.go ├── cover_test.go ├── dovecot_sasl_test.go ├── dovecot_sasld_test.go ├── gocovcat.go ├── golangci-noisy.yml ├── imap_test.go ├── imapsql_test.go ├── issue327_test.go ├── limits_test.go ├── lmtp_test.go ├── mta_test.go ├── multiple_domains_test.go ├── replace_addr_test.go ├── run.sh ├── smtp_autobuffer_test.go ├── smtp_test.go ├── stress_test.go ├── t.go └── testdata ├── check_command.sh ├── testing+addHeader@maddy.test.hdr └── testing+reject@maddy.test.exit /.dockerignore: -------------------------------------------------------------------------------- 1 | testdata/ 2 | cmd/maddy/maddy 3 | maddy 4 | tests/maddy.cover 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.{scd,go}] 9 | indent_style = tab 10 | indent_size = 4 11 | 12 | [*.yml] 13 | indent_style = tab 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: If you think something is broken 4 | title: Bug report 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Describe the bug 11 | 12 | What do you think is wrong? 13 | 14 | # Steps to reproduce 15 | 16 | # Log files 17 | 18 | Use a service like hastebin.com or attach a file if it is big 19 | 20 | # Configuration file 21 | 22 | Located in /etc/maddy/maddy.conf by default, don't forget to remove DB passwords 23 | and other security-related stuff. 24 | 25 | # Environment information 26 | 27 | * maddy version: ? 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Questions 3 | url: "https://github.com/foxcpp/maddy/discussions/new?category=q-a" 4 | about: "Use GitHub discussions for any questions" 5 | - name: IRC channel 6 | url: "https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1" 7 | about: "... or there is also an IRC channel for any discussions" 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: If you would like to see a new feature in maddy. 4 | title: Feature request 5 | labels: new feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Use case 11 | 12 | What problem you are trying to solve? 13 | 14 | Note alternatives you considered and why they are not useful. 15 | 16 | # Your idea for a solution 17 | 18 | How your solution would work in general? 19 | Note that some overly complicated solutions may be rejected because maddy is 20 | meant to be simple. 21 | 22 | - [ ] I'm willing to help with the implementation 23 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Two latest incompatible releases (e.g. 2.0.0 and 1.9.0). 6 | 7 | Latest release gets all bug fixes, features, etc. Previous incompatible release 8 | gets security fixes and fixes for problems that render software completely 9 | unusable in certain configurations with no workaround. 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you believe the vulnerabilitiy does have a big impact on existing 14 | deployments - email `fox.cpp at disroot.org`, put "[maddy security]" in the 15 | Subject. 16 | 17 | Otherwise, open a public issue. 18 | -------------------------------------------------------------------------------- /.github/releases.md: -------------------------------------------------------------------------------- 1 | # Release preparation 2 | 3 | 1. Run linters, fix all simple warnings. If the behavior is intentional - add 4 | `nolint` comment and explanation. If the warning is non-trviail to fix - open 5 | an issue. 6 | ``` 7 | golangci-lint run 8 | ``` 9 | 10 | 2. Run unit tests suite. Verify that all disabled tests are not related to 11 | serious problems and have corresponding issue open. 12 | ``` 13 | go test ./... 14 | ``` 15 | 16 | 3. Run integration tests suite. Verify that all disabled tests are not related 17 | to serious problems and have corresponding issue open. 18 | ``` 19 | cd tests/ 20 | ./run.sh 21 | ``` 22 | 23 | 4. Write release notes. 24 | 25 | 5. Create PGP-signed Git tag and push it to GitHub (do not create a "release" 26 | yet). 27 | 28 | 5. Use environment configuration from maddy-repro bundle 29 | (https://foxcpp.dev/maddy-repro) to build release artifacts. 30 | 31 | 6. Create detached PGP signatures for artifacts using key 32 | 3197BBD95137E682A59717B434BB2007081396F4. 33 | 34 | 7. Create sha256sums file for artifacts. 35 | 36 | 8. Create release on GitHub using the same text for 37 | release notes. Attach signed artifacts and sha256sums file. 38 | 39 | 9. Build the Docker container and push it to hub.docker.com. 40 | 41 | 10. Post a message on the sr.ht mailing list. 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Testing" 2 | 3 | on: 4 | push: 5 | branches: [ master, dev ] 6 | tags: [ "v*" ] 7 | pull_request: 8 | branches: [ master, dev ] 9 | 10 | permissions: 11 | contents: read 12 | pull-requests: read 13 | checks: write 14 | 15 | jobs: 16 | golangci: 17 | name: Lint 18 | runs-on: ubuntu-24.04 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version-file: 'go.mod' 24 | - name: "Install libpam" 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get install -y libpam-dev 28 | - uses: golangci/golangci-lint-action@v6 29 | with: 30 | version: v1.60 31 | args: "--timeout=30m" 32 | buildsh: 33 | name: "Verify build.sh" 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-go@v5 38 | with: 39 | go-version-file: 'go.mod' 40 | - name: "Install libpam" 41 | run: | 42 | sudo apt-get update 43 | sudo apt-get install -y libpam-dev 44 | - name: "Verify build.sh" 45 | run: | 46 | ./build.sh 47 | ./build.sh --destdir destdir/ install 48 | find destdir/ 49 | test: 50 | name: "Build and test" 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v4 54 | - uses: actions/setup-go@v5 55 | with: 56 | go-version-file: 'go.mod' 57 | - name: "Install libpam" 58 | run: | 59 | sudo apt-get update 60 | sudo apt-get install -y libpam-dev 61 | - name: "Unit & module tests" 62 | run: | 63 | go test ./... -coverprofile=coverage.out -covermode=atomic 64 | - name: "Integration tests" 65 | run: | 66 | cd tests/ 67 | ./run.sh 68 | - uses: codecov/codecov-action@v2 69 | with: 70 | files: ./coverage.out 71 | flags: unit 72 | - uses: codecov/codecov-action@v2 73 | with: 74 | files: ./tests/coverage.out 75 | flags: integration 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gitignore.io 2 | *.o 3 | *.a 4 | *.so 5 | _obj 6 | _test 7 | *.[568vq] 8 | [568vq].out 9 | *.cgo1.go 10 | *.cgo2.c 11 | _cgo_defun.c 12 | _cgo_gotypes.go 13 | _cgo_export.* 14 | _testmain.go 15 | *.exe 16 | *.exe~ 17 | *.test 18 | *.prof 19 | **/.envrc 20 | **/.DS_Store 21 | 22 | # Tests coverage 23 | *.out 24 | 25 | # Compiled binaries 26 | cmd/maddy/maddy 27 | cmd/maddy-*-helper/maddy-*-helper 28 | /maddy 29 | 30 | # Man pages 31 | docs/man/*.1 32 | docs/man/*.5 33 | 34 | # Certificates and private keys. 35 | *.pem 36 | *.crt 37 | *.key 38 | 39 | # Some directories that may be created during test-runs 40 | # in repo directory. 41 | cmd/maddy/*mtasts-cache 42 | cmd/maddy/*queue 43 | 44 | build/ 45 | 46 | tests/maddy.cover 47 | tests/maddy 48 | 49 | .idea/ 50 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gosimple 4 | - errcheck 5 | - staticcheck 6 | - ineffassign 7 | - typecheck 8 | - govet 9 | - unused 10 | - goimports 11 | - prealloc 12 | - unconvert 13 | - misspell 14 | - whitespace 15 | - nakedret 16 | - dogsled 17 | - copyloopvar 18 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 0.8.1 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-alpine AS build-env 2 | 3 | ARG ADDITIONAL_BUILD_TAGS="" 4 | 5 | RUN set -ex && \ 6 | apk upgrade --no-cache --available && \ 7 | apk add --no-cache build-base 8 | 9 | WORKDIR /maddy 10 | 11 | COPY go.mod go.sum ./ 12 | RUN go mod download 13 | 14 | COPY . ./ 15 | RUN mkdir -p /pkg/data && \ 16 | cp maddy.conf.docker /pkg/data/maddy.conf && \ 17 | ./build.sh --builddir /tmp --destdir /pkg/ --tags "docker ${ADDITIONAL_BUILD_TAGS}" build install 18 | 19 | FROM alpine:3.21.2 20 | LABEL maintainer="fox.cpp@disroot.org" 21 | LABEL org.opencontainers.image.source=https://github.com/foxcpp/maddy 22 | 23 | RUN set -ex && \ 24 | apk upgrade --no-cache --available && \ 25 | apk --no-cache add ca-certificates 26 | COPY --from=build-env /pkg/data/maddy.conf /data/maddy.conf 27 | COPY --from=build-env /pkg/usr/local/bin/maddy /bin/ 28 | 29 | EXPOSE 25 143 993 587 465 30 | VOLUME ["/data"] 31 | ENTRYPOINT ["/bin/maddy", "-config", "/data/maddy.conf"] 32 | CMD ["run"] 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Maddy Mail Server 2 | ===================== 3 | > Composable all-in-one mail server. 4 | 5 | Maddy Mail Server implements all functionality required to run a e-mail 6 | server. It can send messages via SMTP (works as MTA), accept messages via SMTP 7 | (works as MX) and store messages while providing access to them via IMAP. 8 | In addition to that it implements auxiliary protocols that are mandatory 9 | to keep email reasonably secure (DKIM, SPF, DMARC, DANE, MTA-STS). 10 | 11 | It replaces Postfix, Dovecot, OpenDKIM, OpenSPF, OpenDMARC and more with one 12 | daemon with uniform configuration and minimal maintenance cost. 13 | 14 | **Note:** IMAP storage is "beta". If you are looking for stable and 15 | feature-packed implementation you may want to use Dovecot instead. maddy still 16 | can handle message delivery business. 17 | 18 | [![CI status](https://img.shields.io/github/actions/workflow/status/foxcpp/maddy/cicd.yml?style=flat-square)](https://github.com/foxcpp/maddy/actions/workflows/cicd.yml) 19 | [![Issues tracker](https://img.shields.io/github/issues/foxcpp/maddy?style=flat-square)](https://github.com/foxcpp/maddy) 20 | 21 | * [Setup tutorial](https://maddy.email/tutorials/setting-up/) 22 | * [Documentation](https://maddy.email/) 23 | 24 | * [IRC channel](https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1) 25 | * [Mailing list](https://lists.sr.ht/~foxcpp/maddy) 26 | -------------------------------------------------------------------------------- /cmd/README.md: -------------------------------------------------------------------------------- 1 | maddy executables 2 | ------------------- 3 | 4 | ### maddy 5 | 6 | Main server executable. 7 | 8 | ### maddy-pam-helper, maddy-shadow-helper 9 | 10 | Utilities compatible with the auth.external module that call libpam or read 11 | /etc/shadow on Unix systems. 12 | -------------------------------------------------------------------------------- /cmd/maddy-pam-helper/README.md: -------------------------------------------------------------------------------- 1 | ## maddy-pam-helper 2 | 3 | External setuid binary for interaction with shadow passwords database or other 4 | privileged objects necessary to run PAM authentication. 5 | 6 | ### Building 7 | 8 | It is really easy to build it using any GCC: 9 | ``` 10 | gcc pam.c main.c -lpam -o maddy-pam-helper 11 | ``` 12 | 13 | Yes, it is not a Go binary. 14 | 15 | 16 | ### Installation 17 | 18 | maddy-pam-helper is kinda dangerous binary and should not be allowed to be 19 | executed by everybody but maddy's user. At the same moment it needs to have 20 | access to read-protected files. For this reason installation should be done 21 | very carefully to make sure to not introduce any security "holes". 22 | 23 | #### First method 24 | 25 | ```shell 26 | chown maddy: /usr/bin/maddy-pam-helper 27 | chmod u+x,g-x,o-x /usr/bin/maddy-pam-helper 28 | ``` 29 | 30 | Also maddy-pam-helper needs access to /etc/shadow, one of the ways to provide 31 | it is to set file capability CAP_DAC_READ_SEARCH: 32 | ``` 33 | setcap cap_dac_read_search+ep /usr/bin/maddy-pam-helper 34 | ``` 35 | 36 | #### Second method 37 | 38 | Another, less restrictive is to make it setuid-root (assuming you have both maddy user and group): 39 | ``` 40 | chown root:maddy /usr/bin/maddy-pam-helper 41 | chmod u+xs,g+x,o-x /usr/bin/maddy-pam-helper 42 | ``` 43 | 44 | #### Third method 45 | 46 | The best way actually is to create `shadow` group and grant access to 47 | /etc/shadow to it and then make maddy-pam-helper setgid-shadow: 48 | ``` 49 | groupadd shadow 50 | chown :shadow /etc/shadow 51 | chmod g+r /etc/shadow 52 | chown maddy:shadow /usr/bin/maddy-pam-helper 53 | chmod u+x,g+xs /usr/bin/maddy-pam-helper 54 | ``` 55 | 56 | Pick what works best for you. 57 | 58 | ### PAM service 59 | 60 | maddy-pam-helper uses custom service instead of pretending to be su or sudo. 61 | Because of this you should configure PAM to accept it. 62 | 63 | Minimal example using local passwd/shadow database for authentication can be 64 | found in [maddy.conf][maddy.conf] file. 65 | It should be put into /etc/pam.d/maddy. 66 | -------------------------------------------------------------------------------- /cmd/maddy-pam-helper/maddy.conf: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | auth required pam_unix.so 3 | account required pam_unix.so 4 | -------------------------------------------------------------------------------- /cmd/maddy-pam-helper/main.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #include 3 | #include 4 | #include 5 | #include "pam.h" 6 | 7 | /* 8 | I really doubt it is a good idea to bring Go to the binary whose primary task 9 | is to call libpam using CGo anyway. 10 | */ 11 | 12 | int run(void) { 13 | char *username = NULL, *password = NULL; 14 | size_t username_buf_len = 0, password_buf_len = 0; 15 | 16 | ssize_t username_len = getline(&username, &username_buf_len, stdin); 17 | if (username_len < 0) { 18 | perror("getline username"); 19 | return 2; 20 | } 21 | 22 | ssize_t password_len = getline(&password, &password_buf_len, stdin); 23 | if (password_len < 0) { 24 | perror("getline password"); 25 | return 2; 26 | } 27 | 28 | // Cut trailing \n. 29 | if (username_len > 0) { 30 | username[username_len - 1] = 0; 31 | } 32 | if (password_len > 0) { 33 | password[password_len - 1] = 0; 34 | } 35 | 36 | struct error_obj err = run_pam_auth(username, password); 37 | if (err.status != 0) { 38 | if (err.status == 2) { 39 | fprintf(stderr, "%s: %s\n", err.func_name, err.error_msg); 40 | } 41 | return err.status; 42 | } 43 | 44 | return 0; 45 | } 46 | 47 | #ifndef CGO 48 | int main() { 49 | return run(); 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /cmd/maddy-pam-helper/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | /* 22 | #cgo LDFLAGS: -lpam 23 | #cgo CFLAGS: -DCGO -Wall -Wextra -Werror -Wno-unused-parameter -Wno-error=unused-parameter -Wpedantic -std=c99 24 | extern int run(); 25 | */ 26 | import "C" 27 | import "os" 28 | 29 | /* 30 | Apparently, some people would not want to build it manually by calling GCC. 31 | Here we do it for them. Not going to tell them that resulting file is 800KiB 32 | bigger than one built using only C compiler. 33 | */ 34 | 35 | func main() { 36 | i := int(C.run()) 37 | os.Exit(i) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/maddy-pam-helper/pam.h: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | struct error_obj { 22 | int status; 23 | const char* func_name; 24 | const char* error_msg; 25 | }; 26 | 27 | struct error_obj run_pam_auth(const char *username, char *password); 28 | -------------------------------------------------------------------------------- /cmd/maddy-shadow-helper/README.md: -------------------------------------------------------------------------------- 1 | ## maddy-shadow-helper 2 | 3 | External helper binary for interaction with shadow passwords database. 4 | Unlike maddy-pam-helper it supports only local shadow database but it does 5 | not have any C dependencies. 6 | 7 | ### Installation 8 | 9 | maddy-shadow-helper is kinda dangerous binary and should not be allowed to be 10 | executed by everybody but maddy's user. At the same moment it needs to have 11 | access to read-protected files. For this reason installation should be done 12 | very carefully to make sure to not introduce any security "holes". 13 | 14 | #### First method 15 | 16 | ```shell 17 | chown maddy: /usr/bin/maddy-shadow-helper 18 | chmod u+x,g-x,o-x /usr/bin/maddy-shadow-helper 19 | ``` 20 | 21 | Also maddy-shadow-helper needs access to /etc/shadow, one of the ways to provide 22 | it is to set file capability CAP_DAC_READ_SEARCH: 23 | ``` 24 | setcap cap_dac_read_search+ep /usr/bin/maddy-shadow-helper 25 | ``` 26 | 27 | #### Second method 28 | 29 | Another, less restrictive is to make it setuid-root (assuming you have both maddy user and group): 30 | ``` 31 | chown root:maddy /usr/bin/maddy-shadow-helper 32 | chmod u+xs,g+x,o-x /usr/bin/maddy-shadow-helper 33 | ``` 34 | 35 | #### Third method 36 | 37 | The best way actually is to create `shadow` group and grant access to 38 | /etc/shadow to it and then make maddy-shadow-helper setgid-shadow: 39 | ``` 40 | groupadd shadow 41 | chown :shadow /etc/shadow 42 | chmod g+r /etc/shadow 43 | chown maddy:shadow /usr/bin/maddy-shadow-helper 44 | chmod u+x,g+xs /usr/bin/maddy-shadow-helper 45 | ``` 46 | 47 | Pick what works best for you. 48 | -------------------------------------------------------------------------------- /cmd/maddy-shadow-helper/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "bufio" 23 | "errors" 24 | "fmt" 25 | "os" 26 | 27 | "github.com/foxcpp/maddy/internal/auth/shadow" 28 | ) 29 | 30 | func main() { 31 | scnr := bufio.NewScanner(os.Stdin) 32 | 33 | if !scnr.Scan() { 34 | fmt.Fprintln(os.Stderr, scnr.Err()) 35 | os.Exit(2) 36 | } 37 | username := scnr.Text() 38 | 39 | if !scnr.Scan() { 40 | fmt.Fprintln(os.Stderr, scnr.Err()) 41 | os.Exit(2) 42 | } 43 | password := scnr.Text() 44 | 45 | ent, err := shadow.Lookup(username) 46 | if err != nil { 47 | if errors.Is(err, shadow.ErrNoSuchUser) { 48 | os.Exit(1) 49 | } 50 | fmt.Fprintln(os.Stderr, err) 51 | os.Exit(2) 52 | } 53 | 54 | if !ent.IsAccountValid() { 55 | fmt.Fprintln(os.Stderr, "account is expired") 56 | os.Exit(1) 57 | } 58 | 59 | if !ent.IsPasswordValid() { 60 | fmt.Fprintln(os.Stderr, "password is expired") 61 | os.Exit(1) 62 | } 63 | 64 | if err := ent.VerifyPassword(password); err != nil { 65 | if errors.Is(err, shadow.ErrWrongPassword) { 66 | os.Exit(1) 67 | } 68 | fmt.Fprintln(os.Stderr, err) 69 | os.Exit(1) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/maddy/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | _ "github.com/foxcpp/maddy" 23 | maddycli "github.com/foxcpp/maddy/internal/cli" 24 | _ "github.com/foxcpp/maddy/internal/cli/ctl" 25 | ) 26 | 27 | func main() { 28 | maddycli.Run() 29 | } 30 | -------------------------------------------------------------------------------- /contrib/README.md: -------------------------------------------------------------------------------- 1 | # Community contributed resources 2 | 3 | Disclaimer: Nothing inside subdirectories here is directly supported by Maddy 4 | Mail Server maintainers. Some community members may be able to help you or not. 5 | 6 | - Kubernetes helm chart is maintained by @acim. 7 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: maddy 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.2.6 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | appVersion: 0.4.0 24 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/files/aliases: -------------------------------------------------------------------------------- 1 | info@example.org: foxcpp@example.org 2 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxcpp/maddy/fa47d40f6d510a431d2bbc238c7d36a58774ae2f/contrib/kubernetes/chart/templates/NOTES.txt -------------------------------------------------------------------------------- /contrib/kubernetes/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "maddy.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "maddy.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "maddy.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "maddy.labels" -}} 37 | helm.sh/chart: {{ include "maddy.chart" . }} 38 | {{ include "maddy.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "maddy.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "maddy.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "maddy.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "maddy.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{include "maddy.fullname" .}} 5 | labels: {{- include "maddy.labels" . | nindent 4}} 6 | data: 7 | maddy.conf: | 8 | {{ tpl (.Files.Get "files/maddy.conf") . | printf "%s" | indent 4 }} 9 | aliases: | 10 | {{ tpl (.Files.Get "files/aliases") . | printf "%s" | indent 4 }} 11 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/templates/pvc.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) -}} 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | name: {{ include "maddy.fullname" . }} 6 | annotations: 7 | {{- with .Values.persistence.annotations }} 8 | {{ toYaml . | indent 4 }} 9 | {{- end }} 10 | labels: 11 | {{- include "maddy.labels" . | nindent 4 }} 12 | spec: 13 | accessModes: 14 | - {{ .Values.persistence.accessMode }} 15 | resources: 16 | requests: 17 | storage: {{ .Values.persistence.size }} 18 | {{- if .Values.persistence.storageClass }} 19 | storageClassName: {{ .Values.persistence.storageClass }} 20 | {{- end }} 21 | {{- end -}} 22 | 23 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "maddy.fullname" . }} 5 | labels: 6 | {{- include "maddy.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: 25 11 | targetPort: smtp 12 | protocol: TCP 13 | name: smtp 14 | - port: 993 15 | targetPort: imaps 16 | protocol: TCP 17 | name: imaps 18 | - port: 587 19 | targetPort: starttls 20 | protocol: TCP 21 | name: starttls 22 | selector: 23 | {{- include "maddy.selectorLabels" . | nindent 4 }} 24 | {{- with .Values.service.externalIPs }} 25 | externalIPs: 26 | {{- toYaml . | nindent 6 }} 27 | {{- end -}} 28 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "maddy.serviceAccountName" . }} 6 | labels: 7 | {{- include "maddy.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "maddy.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "maddy.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "maddy.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /contrib/kubernetes/chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for maddy. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 # Multiple replicas are not supported, please don't change this. 6 | 7 | image: 8 | repository: foxcpp/maddy 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: 29 | {} 30 | # fsGroup: 2000 31 | 32 | securityContext: 33 | {} 34 | # capabilities: 35 | # drop: 36 | # - ALL 37 | # readOnlyRootFilesystem: true 38 | # runAsNonRoot: true 39 | # runAsUser: 1000 40 | 41 | # Set externalPIs to your public IP(s) of the node running maddy. In case of multiple nodes, you need to set tolerations 42 | # and taints in order to run maddy on the exact node. 43 | service: 44 | type: NodePort 45 | # externalIPs: 46 | 47 | resources: 48 | {} 49 | # We usually recommend not to specify default resources and to leave this as a conscious 50 | # choice for the user. This also increases chances charts run on environments with little 51 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 52 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 53 | # limits: 54 | # cpu: 100m 55 | # memory: 128Mi 56 | # requests: 57 | # cpu: 100m 58 | # memory: 128Mi 59 | 60 | persistence: 61 | enabled: false 62 | # existingClaim: "" 63 | accessMode: ReadWriteOnce 64 | size: 128Mi 65 | # storageClass: "" 66 | path: /data 67 | annotations: {} 68 | # subPath: "" # only mount a subpath of the Volume into the pod 69 | 70 | nodeSelector: {} 71 | 72 | tolerations: [] 73 | 74 | affinity: {} 75 | -------------------------------------------------------------------------------- /directories.go: -------------------------------------------------------------------------------- 1 | //go:build !docker 2 | // +build !docker 3 | 4 | package maddy 5 | 6 | var ( 7 | // ConfigDirectory specifies platform-specific value 8 | // that should be used as a location of default configuration 9 | // 10 | // It should not be changed and is defined as a variable 11 | // only for purposes of modification using -X linker flag. 12 | ConfigDirectory = "/etc/maddy" 13 | 14 | // DefaultStateDirectory specifies platform-specific 15 | // default for StateDirectory. 16 | // 17 | // Most code should use StateDirectory instead since 18 | // it will contain the effective location of the state 19 | // directory. 20 | // 21 | // It should not be changed and is defined as a variable 22 | // only for purposes of modification using -X linker flag. 23 | DefaultStateDirectory = "/var/lib/maddy" 24 | 25 | // DefaultRuntimeDirectory specifies platform-specific 26 | // default for RuntimeDirectory. 27 | // 28 | // Most code should use RuntimeDirectory instead since 29 | // it will contain the effective location of the state 30 | // directory. 31 | // 32 | // It should not be changed and is defined as a variable 33 | // only for purposes of modification using -X linker flag. 34 | DefaultRuntimeDirectory = "/run/maddy" 35 | 36 | // DefaultLibexecDirectory specifies platform-specific 37 | // default for LibexecDirectory. 38 | // 39 | // Most code should use LibexecDirectory since it will 40 | // contain the effective location of the libexec 41 | // directory. 42 | // 43 | // It should not be changed and is defined as a variable 44 | // only for purposes of modification using -X linker flag. 45 | DefaultLibexecDirectory = "/usr/lib/maddy" 46 | ) 47 | -------------------------------------------------------------------------------- /directories_docker.go: -------------------------------------------------------------------------------- 1 | //go:build docker 2 | // +build docker 3 | 4 | package maddy 5 | 6 | var ( 7 | ConfigDirectory = "/data" 8 | DefaultStateDirectory = "/data" 9 | DefaultRuntimeDirectory = "/tmp" 10 | DefaultLibexecDirectory = "/usr/lib/maddy" 11 | ) 12 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | Distribution files for maddy 2 | ------------------------------ 3 | 4 | **Disclaimer:** Most of the files here are maintained in a "best-effort" way. 5 | That is, they may break or become outdated from time to time. Caveat emptor. 6 | 7 | ## integration + scripts 8 | 9 | These directories provide pre-made configuration snippets suitable for 10 | easy integration with external software. 11 | 12 | Usually, this is what you use when you put `import integration/something` in 13 | your config. 14 | 15 | ## systemd unit 16 | 17 | `maddy.service` launches using default config path (/etc/maddy/maddy.conf). 18 | `maddy@.service` launches maddy using custom config path. E.g. 19 | `maddy@foo.service` will use /etc/maddy/foo.conf. 20 | 21 | Additionally, unit files apply strict sandboxing, limiting maddy permissions on 22 | the system to a bare minimum. Subset of these options makes it impossible for 23 | privileged authentication helper binaries to gain required permissions, so you 24 | may have to disable it when using system account-based authentication with 25 | maddy running as a unprivileged user. 26 | 27 | ## fail2ban configuration 28 | 29 | Configuration files for use with fail2ban. Assume either `backend = systemd` specified 30 | in system-wide configuration or log file written to /var/log/maddy/maddy.log. 31 | 32 | See https://github.com/foxcpp/maddy/wiki/fail2ban-configuration for details. 33 | 34 | ## logrotate configuration 35 | 36 | Meant for logs rotation when logging to file is used. 37 | 38 | ## vim ftdetect/ftplugin/syntax files 39 | 40 | Minimal supplement to make configuration files more readable and help you see 41 | typos in directive names. 42 | -------------------------------------------------------------------------------- /dist/apparmor/dev.foxcpp.maddy: -------------------------------------------------------------------------------- 1 | # AppArmor profile for maddy daemon. 2 | # vim:syntax=apparmor:ts=2:sw=2:et 3 | 4 | #include 5 | 6 | profile dev.foxcpp.maddy /usr{/local,}/bin/maddy { 7 | #include 8 | #include 9 | #include 10 | /etc/ca-certificates/** r, 11 | 12 | /etc/resolv.conf r, 13 | /proc/sys/net/core/somaxconn r, 14 | /sys/kernel/mm/transparent_hugepage/hpage_pmd_size r, 15 | deny ptrace, 16 | capability net_bind_service, 17 | network tcp, 18 | network unix, 19 | 20 | # systemd process management and Type=notify 21 | signal (receive) peer=unconfined, 22 | signal (receive) peer=/usr/bin/systemd, 23 | unix (create, connect, send, setopt) type=dgram addr=@*, 24 | /run/systemd/notify w, 25 | 26 | /etc/maddy/** r, 27 | owner /run/maddy/ rw, 28 | owner /run/maddy/** rwkl, 29 | owner /var/lib/maddy/ rw, 30 | owner /var/lib/maddy/** rwk, 31 | owner /var/lib/maddy/**.db-{wal,shm} rmk, 32 | 33 | /usr{/local,}/lib/maddy/* PUx, 34 | 35 | /usr{/local,}/bin/maddy{,ctl} rmix, 36 | 37 | #include if exists 38 | } 39 | -------------------------------------------------------------------------------- /dist/fail2ban/filter.d/maddy-auth.conf: -------------------------------------------------------------------------------- 1 | [INCLUDES] 2 | before = common.conf 3 | 4 | [Definition] 5 | failregex = authentication failed\t\{\"reason\":\".*\",\"src_ip\"\:\":\d+\"\,\"username\"\:\".*\"\}$ 6 | journalmatch = _SYSTEMD_UNIT=maddy.service + _COMM=maddy 7 | -------------------------------------------------------------------------------- /dist/fail2ban/filter.d/maddy-dictonary-attack.conf: -------------------------------------------------------------------------------- 1 | [INCLUDES] 2 | before = common.conf 3 | 4 | [Definition] 5 | failregex = smtp\: MAIL FROM error repeated a lot\, possible dictonary attack\t\{\"count\"\:\d+,\"msg_id\":\".+\",\"src_ip\"\:\":\d+\"\}$ 6 | smtp\: too many RCPT errors\, possible dictonary attack\t\{\"msg_id\":\".+\","src_ip":":\d+\"\} 7 | journalmatch = _SYSTEMD_UNIT=maddy.service + _COMM=maddy 8 | -------------------------------------------------------------------------------- /dist/fail2ban/jail.d/maddy-auth.conf: -------------------------------------------------------------------------------- 1 | [maddy-auth] 2 | port = 993,465,25 3 | filter = maddy-auth 4 | bantime = 96h 5 | backend = systemd 6 | -------------------------------------------------------------------------------- /dist/fail2ban/jail.d/maddy-dictonary-attack.conf: -------------------------------------------------------------------------------- 1 | [maddy-dictonary-attack] 2 | port = 993,465,25 3 | filter = maddy-dictonary-attack 4 | bantime = 72h 5 | maxretry = 3 6 | findtime = 6h 7 | backend = systemd 8 | -------------------------------------------------------------------------------- /dist/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DESTDIR=$DESTDIR 4 | if [ -z "$PREFIX" ]; then 5 | PREFIX=/usr/local 6 | fi 7 | if [ -z "$FAIL2BANDIR" ]; then 8 | FAIL2BANDIR=/etc/fail2ban 9 | fi 10 | if [ -z "$CONFDIR" ]; then 11 | CONFDIR=/etc/maddy 12 | fi 13 | 14 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 15 | cd $script_dir 16 | 17 | install -Dm 0644 -t "$DESTDIR/$PREFIX/share/vim/vimfiles/ftdetect/" vim/ftdetect/maddy-conf.vim 18 | install -Dm 0644 -t "$DESTDIR/$PREFIX/share/vim/vimfiles/ftplugin/" vim/ftplugin/maddy-conf.vim 19 | install -Dm 0644 -t "$DESTDIR/$PREFIX/share/vim/vimfiles/syntax/" vim/syntax/maddy-conf.vim 20 | 21 | install -Dm 0644 -t "$DESTDIR/$FAIL2BANDIR/jail.d/" fail2ban/jail.d/* 22 | install -Dm 0644 -t "$DESTDIR/$FAIL2BANDIR/filter.d/" fail2ban/filter.d/* 23 | 24 | install -Dm 0644 -t "$DESTDIR/$PREFIX/lib/systemd/system/" systemd/maddy.service systemd/maddy@.service 25 | -------------------------------------------------------------------------------- /dist/logrotate.d/maddy: -------------------------------------------------------------------------------- 1 | /var/log/maddy/maddy.log { 2 | missingok 3 | su maddy maddy 4 | postrotate 5 | /usr/bin/killall -USR1 maddy 6 | endscript 7 | } 8 | -------------------------------------------------------------------------------- /dist/vim/ftdetect/maddy-conf.vim: -------------------------------------------------------------------------------- 1 | au BufNewFile,BufRead /etc/maddy/*,maddy.conf setf maddy-conf 2 | -------------------------------------------------------------------------------- /dist/vim/ftplugin/maddy-conf.vim: -------------------------------------------------------------------------------- 1 | setlocal commentstring=#\ %s 2 | 3 | " That is convention for maddy configs. Period. 4 | " - fox.cpp (maddy developer) 5 | setlocal expandtab 6 | setlocal tabstop=4 7 | setlocal softtabstop=4 8 | setlocal shiftwidth=4 9 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | > Composable all-in-one mail server. 2 | 3 | Maddy Mail Server implements all functionality required to run a e-mail 4 | server. It can send messages via SMTP (works as MTA), accept messages via SMTP 5 | (works as MX) and store messages while providing access to them via IMAP. 6 | In addition to that it implements auxiliary protocols that are mandatory 7 | to keep email reasonably secure (DKIM, SPF, DMARC, DANE, MTA-STS). 8 | 9 | It replaces Postfix, Dovecot, OpenDKIM, OpenSPF, OpenDMARC and more with one 10 | daemon with uniform configuration and minimal maintenance cost. 11 | 12 | **Note:** IMAP storage is "beta". If you are looking for stable and 13 | feature-packed implementation you may want to use Dovecot instead. maddy still 14 | can handle message delivery business. 15 | 16 | [![builds.sr.ht status](https://builds.sr.ht/~emersion/maddy.svg)](https://builds.sr.ht/~emersion/maddy?) 17 | [![License text](https://img.shields.io/github/license/foxcpp/maddy)](https://github.com/foxcpp/maddy/blob/master/LICENSE) 18 | [![Issues tracker](https://img.shields.io/github/issues/foxcpp/maddy)](https://github.com/foxcpp/maddy) 19 | 20 | * [Setup tutorial](https://maddy.email/tutorials/setting-up/) 21 | * [IRC channel](https://webchat.oftc.net/?channels=maddy&uio=MT11bmRlZmluZWQb1) 22 | * [Mailing list](https://lists.sr.ht/~foxcpp/maddy) 23 | -------------------------------------------------------------------------------- /docs/internals/quirks.md: -------------------------------------------------------------------------------- 1 | # Implementation quirks 2 | 3 | This page documents unusual behavior of the maddy protocols implementations. 4 | Some of these problems break standards, some don't but still can hurt 5 | interoperability. 6 | 7 | ## SMTP 8 | 9 | - `for` field is never included in the `Received` header field. 10 | 11 | This is allowed by [RFC 2821]. 12 | 13 | ## IMAP 14 | 15 | ### `sql` 16 | 17 | - `\Recent` flag is not reset in all cases. 18 | 19 | This _does not_ break [RFC 3501]. Clients relying on it will work (much) less 20 | efficiently. 21 | 22 | [RFC 2821]: https://tools.ietf.org/html/rfc2821 23 | [RFC 3501]: https://tools.ietf.org/html/rfc3501 24 | -------------------------------------------------------------------------------- /docs/internals/sqlite.md: -------------------------------------------------------------------------------- 1 | # maddy & SQLite 2 | 3 | SQLite is a perfect choice for small deployments because no additional 4 | configuration is required to get started. It is recommended for cases when you 5 | have less than 10 mail accounts. 6 | 7 | **Note: SQLite requires DB-wide locking for writing, it means that multiple 8 | messages can't be accepted in parallel. This is not the case for server-based 9 | RDBMS where maddy can accept multiple messages in parallel even for a single 10 | mailbox.** 11 | 12 | ## WAL mode 13 | 14 | maddy forces WAL journal mode for SQLite. This makes things reasonably fast and 15 | reduces locking contention which may be important for a typical mail server. 16 | 17 | maddy uses increased WAL autocheckpoint interval. This means that while 18 | maintaining a high write throughput, maddy will have to stop for a bit (0.5-1 19 | second) every time 78 MiB is written to the DB (with default configuration it 20 | is 15 MiB). 21 | 22 | Note that when moving the database around you need to move WAL journal (`-wal`) 23 | and shared memory (`-shm`) files as well, otherwise some changes to the DB will 24 | be lost. 25 | 26 | ## Query planner statistics 27 | 28 | maddy updates query planner statistics on shutdown and every 5 hours. It 29 | provides query planner with information to access the database in more 30 | efficient way because go-imap-sql schema does use a few so called "low-quality 31 | indexes". 32 | 33 | ## Auto-vacuum 34 | 35 | maddy turns on SQLite auto-vacuum feature. This means that database file size 36 | will shrink when data is removed (compared to default when it remains unused). 37 | 38 | ## Manual vacuuming 39 | 40 | Auto-vacuuming can lead to database fragmentation and thus reduce the read 41 | performance. To do manual vacuum operation to repack and defragment database 42 | file, install the SQLite3 console utility and run the following commands: 43 | ``` 44 | sqlite3 -cmd 'vacuum' database_file_path_here.db 45 | sqlite3 -cmd 'pragma wal_checkpoint(truncate)' database_file_path_here.db 46 | ``` 47 | 48 | It will take some time to complete, you can close the utility when the 49 | `sqlite>` prompt appears. 50 | -------------------------------------------------------------------------------- /docs/man/.gitignore: -------------------------------------------------------------------------------- 1 | _generated_*.md 2 | -------------------------------------------------------------------------------- /docs/man/README.md: -------------------------------------------------------------------------------- 1 | maddy manual pages 2 | ------------------- 3 | 4 | The reference documentation is maintained in the scdoc format and is compiled 5 | into a set of Unix man pages viewable using the standard `man` utility. 6 | 7 | See https://git.sr.ht/~sircmpwn/scdoc for information about the tool used to 8 | build pages. 9 | It can be used as follows: 10 | ``` 11 | scdoc < maddy-filters.5.scd > maddy-filters.5 12 | man ./maddy-filters.5 13 | ``` 14 | 15 | build.sh script in the repo root compiles and installs man pages if the scdoc 16 | utility is installed in the system. 17 | -------------------------------------------------------------------------------- /docs/man/maddy.1.scd: -------------------------------------------------------------------------------- 1 | maddy(1) "maddy mail server" "maddy reference documentation" 2 | 3 | ; TITLE Command line arguments 4 | 5 | # Name 6 | 7 | maddy - Composable all-in-one mail server. 8 | 9 | # Synopsis 10 | 11 | *maddy* [options...] 12 | 13 | # Description 14 | 15 | Maddy is Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission 16 | Agent (MSA), IMAP server and a set of other essential protocols/schemes 17 | necessary to run secure email server implemented in one executable. 18 | 19 | # Command line arguments 20 | 21 | *-h, -help* 22 | Show help message and exit. 23 | 24 | *-config* _path_ 25 | Path to the configuration file. Default is /etc/maddy/maddy.conf. 26 | 27 | *-libexec* _path_ 28 | Path to the libexec directory. Helper executables will be searched here. 29 | Default is /usr/lib/maddy. 30 | 31 | *-log* _targets..._ 32 | Comma-separated list of logging targets. Valid values are the same as the 33 | 'log' config directive. Affects logging before configuration parsing 34 | completes and after it, if the different value is not specified in the 35 | configuration. 36 | 37 | *-debug* 38 | Enable debug log. You want to use it when reporting bugs. 39 | 40 | *-v* 41 | Print version & build metadata. 42 | -------------------------------------------------------------------------------- /docs/man/prepare_md.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | This script does all necessary pre-processing to convert scdoc format into 5 | Markdown. 6 | 7 | Usage: 8 | prepare_md.py < in > out 9 | prepare_md.py file1 file2 file3 10 | Converts into _generated_file1.md, etc. 11 | """ 12 | 13 | import sys 14 | import re 15 | 16 | anchor_escape = str.maketrans(r' #()./\+-_', '__________') 17 | 18 | def prepare(r, w): 19 | new_lines = list() 20 | title = str() 21 | previous_h1_anchor = '' 22 | 23 | inside_literal = False 24 | 25 | for line in r: 26 | if not inside_literal: 27 | if line.startswith('; TITLE ') and title == '': 28 | title = line[8:] 29 | if line[0] == ';': 30 | continue 31 | # turn *page*(1) into [**page(1)**](../_generated_page.1) 32 | line = re.sub(r'\*(.+?)\*\(([0-9])\)', r'[*\1(\2)*](../_generated_\1.\2)', line) 33 | # *aaa* => **aaa** 34 | line = re.sub(r'\*(.+?)\*', r'**\1**', line) 35 | # remove ++ from line endings 36 | line = re.sub(r'\+\+$', '
', line) 37 | # turn whatever looks like a link into one 38 | line = re.sub(r'(https://[^ \)\(\\]+[a-z0-9_\-])', r'[\1](\1)', line) 39 | # escape underscores inside words 40 | line = re.sub(r'([^ ])_([^ ])', r'\1\\_\2', line) 41 | 42 | if line.startswith('```'): 43 | inside_literal = not inside_literal 44 | 45 | new_lines.append(line) 46 | 47 | if title != '': 48 | print('#', title, file=w) 49 | 50 | print(''.join(new_lines[1:]), file=w) 51 | 52 | if len(sys.argv) == 1: 53 | prepare(sys.stdin, sys.stdout) 54 | else: 55 | for f in sys.argv[1:]: 56 | new_name = '_generated_' + f[:-4] + '.md' 57 | prepare(open(f, 'r'), open(new_name, 'w')) 58 | -------------------------------------------------------------------------------- /docs/reference/auth/dovecot_sasl.md: -------------------------------------------------------------------------------- 1 | # Dovecot SASL 2 | 3 | The 'auth.dovecot_sasl' module implements the client side of the Dovecot 4 | authentication protocol, allowing maddy to use it as a credentials source. 5 | 6 | Currently SASL mechanisms support is limited to mechanisms supported by maddy 7 | so you cannot get e.g. SCRAM-MD5 this way. 8 | 9 | ``` 10 | auth.dovecot_sasl { 11 | endpoint unix://socket_path 12 | } 13 | 14 | dovecot_sasl unix://socket_path 15 | ``` 16 | 17 | ## Configuration directives 18 | 19 | ### endpoint _schema://address_ 20 | Default: not set 21 | 22 | Set the address to use to contact Dovecot SASL server in the standard endpoint 23 | format. 24 | 25 | `tcp://10.0.0.1:2222` for TCP, `unix:///var/lib/dovecot/auth.sock` for Unix 26 | domain sockets. 27 | -------------------------------------------------------------------------------- /docs/reference/auth/external.md: -------------------------------------------------------------------------------- 1 | # System command 2 | 3 | auth.external module for authentication using external helper binary. It looks for binary 4 | named `maddy-auth-helper` in $PATH and libexecdir and uses it for authentication 5 | using username/password pair. 6 | 7 | The protocol is very simple: 8 | Program is launched for each authentication. Username and password are written 9 | to stdin, adding \n to the end. If binary exits with 0 status code - 10 | authentication is considered successful. If the status code is 1 - 11 | authentication is failed. If the status code is 2 - another unrelated error has 12 | happened. Additional information should be written to stderr. 13 | 14 | ``` 15 | auth.external { 16 | helper /usr/bin/ldap-helper 17 | perdomain no 18 | domains example.org 19 | } 20 | ``` 21 | 22 | ## Configuration directives 23 | 24 | ### helper _file_path_ 25 | 26 | **Required.**
27 | Location of the helper binary. 28 | 29 | --- 30 | 31 | ### perdomain _boolean_ 32 | Default: `no` 33 | 34 | Don't remove domain part of username when authenticating and require it to be 35 | present. Can be used if you want user@domain1 and user@domain2 to be different 36 | accounts. 37 | 38 | --- 39 | 40 | ### domains _domains..._ 41 | Default: not specified 42 | 43 | Domains that should be allowed in username during authentication. 44 | 45 | For example, if 'domains' is set to "domain1 domain2", then 46 | username, username@domain1 and username@domain2 will be accepted as valid login 47 | name in addition to just username. 48 | 49 | If used without 'perdomain', domain part will be removed from login before 50 | check with underlying auth. mechanism. If 'perdomain' is set, then 51 | domains must be also set and domain part **will not** be removed before check. 52 | 53 | -------------------------------------------------------------------------------- /docs/reference/auth/netauth.md: -------------------------------------------------------------------------------- 1 | # Native NetAuth 2 | 3 | maddy supports authentication via NetAuth using direct entity 4 | authentication checks. Passwords are verified by the NetAuth server. 5 | 6 | maddy needs to know the Entity ID to use for authentication. It must 7 | match the string the user provides for the Local Atom part of their 8 | mail address. 9 | 10 | Note that storage backends conventionally use email addresses. Since 11 | NetAuth recommends *nix compatible usernames, you will need to map the 12 | email identifiers to NetAuth Entity IDs using `auth_map` (see 13 | documentation page for used storage backend). 14 | 15 | auth.netauth also can be used as a table module. This way you can 16 | check whether the account exists. 17 | 18 | Note that the configuration fragment provided below is very sparse. 19 | This is because NetAuth expects to read most of its common 20 | configuration values from the system NetAuth config file located at 21 | `/etc/netauth/config.toml`. 22 | 23 | ``` 24 | auth.netauth { 25 | require_group "maddy-users" 26 | debug off 27 | } 28 | ``` 29 | 30 | ``` 31 | auth.netauth {} 32 | ``` 33 | 34 | ## Configuration directives 35 | 36 | ### require_group _group_ 37 | 38 | Optional. 39 | 40 | Group that entities must possess to be able to use maddy services. 41 | This can be used to provide email to just a subset of the entities 42 | present in NetAuth. 43 | 44 | --- 45 | 46 | ### debug `on` | `off` 47 | 48 | Default: `off` 49 | -------------------------------------------------------------------------------- /docs/reference/auth/pam.md: -------------------------------------------------------------------------------- 1 | # PAM 2 | 3 | auth.pam module implements authentication using libpam. Alternatively it can be configured to 4 | use helper binary like auth.external module does. 5 | 6 | maddy should be built with libpam build tag to use this module without 7 | 'use_helper' directive. 8 | 9 | ``` 10 | go get -tags 'libpam' ... 11 | ``` 12 | 13 | ``` 14 | auth.pam { 15 | debug no 16 | use_helper no 17 | } 18 | ``` 19 | 20 | ## Configuration directives 21 | 22 | ### debug _boolean_ 23 | Default: `no` 24 | 25 | Enable verbose logging for all modules. You don't need that unless you are 26 | reporting a bug. 27 | 28 | --- 29 | 30 | ### use_helper _boolean_ 31 | Default: `no` 32 | 33 | Use `LibexecDirectory/maddy-pam-helper` instead of directly calling libpam. 34 | You need to use that if: 35 | 36 | 1. maddy is not compiled with libpam, but `maddy-pam-helper` is built separately. 37 | 2. maddy is running as an unprivileged user and used PAM configuration requires additional privileges (e.g. when using system accounts). 38 | 39 | For 2, you need to make `maddy-pam-helper` binary setuid, see 40 | README.md in source tree for details. 41 | 42 | TL;DR (assuming you have the maddy group): 43 | 44 | ``` 45 | chown root:maddy /usr/lib/maddy/maddy-pam-helper 46 | chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helper 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /docs/reference/auth/pass_table.md: -------------------------------------------------------------------------------- 1 | # Password table 2 | 3 | auth.pass_table module implements username:password authentication by looking up the 4 | password hash using a table module (maddy-tables(5)). It can be used 5 | to load user credentials from text file (via table.file module) or SQL query 6 | (via table.sql_table module). 7 | 8 | 9 | Definition: 10 | ``` 11 | auth.pass_table [block name] { 12 | table 13 | 14 | } 15 | ``` 16 | Shortened variant for inline use: 17 | ``` 18 | pass_table
[table arguments] { 19 | [additional table config] 20 | } 21 | ``` 22 | 23 | Example, read username:password pair from the text file: 24 | ``` 25 | smtp tcp://0.0.0.0:587 { 26 | auth pass_table file /etc/maddy/smtp_passwd 27 | ... 28 | } 29 | ``` 30 | 31 | ## Password hashes 32 | 33 | pass_table expects the used table to contain certain structured values with 34 | hash algorithm name, salt and other necessary parameters. 35 | 36 | You should use `maddy hash` command to generate suitable values. 37 | See `maddy hash --help` for details. 38 | 39 | ## maddy creds 40 | 41 | If the underlying table is a "mutable" table (see maddy-tables(5)) then 42 | the `maddy creds` command can be used to modify the underlying tables 43 | via pass_table module. It will act on a "local credentials store" and will write 44 | appropriate hash values to the table. 45 | -------------------------------------------------------------------------------- /docs/reference/auth/plain_separate.md: -------------------------------------------------------------------------------- 1 | # Separate username and password lookup 2 | 3 | auth.plain_separate module implements authentication using username:password pairs but can 4 | use zero or more "table modules" (maddy-tables(5)) and one or more 5 | authentication providers to verify credentials. 6 | 7 | ``` 8 | auth.plain_separate { 9 | user ... 10 | user ... 11 | ... 12 | pass ... 13 | pass ... 14 | ... 15 | } 16 | ``` 17 | 18 | How it works: 19 | - Initial username input is normalized using PRECIS UsernameCaseMapped profile. 20 | - Each table specified with the 'user' directive looked up using normalized 21 | username. If match is not found in any table, authentication fails. 22 | - Each authentication provider specified with the 'pass' directive is tried. 23 | If authentication with all providers fails - an error is returned. 24 | 25 | ## Configuration directives 26 | 27 | ### user _table-module_ 28 | 29 | Configuration block for any module from maddy-tables(5) can be used here. 30 | 31 | Example: 32 | 33 | ``` 34 | user file /etc/maddy/allowed_users 35 | ``` 36 | 37 | --- 38 | 39 | ### pass _auth-provider_ 40 | 41 | Configuration block for any auth. provider module can be used here, even 42 | 'plain_split' itself. 43 | 44 | The used auth. provider must provide username:password pair-based 45 | authentication. 46 | -------------------------------------------------------------------------------- /docs/reference/auth/shadow.md: -------------------------------------------------------------------------------- 1 | # /etc/shadow 2 | 3 | auth.shadow module implements authentication by reading /etc/shadow. Alternatively it can be 4 | configured to use helper binary like auth.external does. 5 | 6 | ``` 7 | auth.shadow { 8 | debug no 9 | use_helper no 10 | } 11 | ``` 12 | 13 | ## Configuration directives 14 | 15 | ### debug _boolean_ 16 | 17 | Default: `no` 18 | 19 | Enable verbose logging for all modules. You don't need that unless you are 20 | reporting a bug. 21 | 22 | --- 23 | 24 | ### use_helper _boolean_ 25 | Default: `no` 26 | 27 | Use `LibexecDirectory/maddy-shadow-helper` instead of directly reading `/etc/shadow`. 28 | You need to use that if maddy is running as an unprivileged user 29 | privileges (e.g. when using system accounts). 30 | 31 | You need to make `maddy-shadow-helper` binary setuid, see 32 | cmd/maddy-shadow-helper/README.md in source tree for details. 33 | 34 | TL;DR (assuming you have maddy group): 35 | 36 | ``` 37 | chown root:maddy /usr/lib/maddy/maddy-shadow-helper 38 | chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-shadow-helper 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /docs/reference/blob/fs.md: -------------------------------------------------------------------------------- 1 | # Filesystem 2 | 3 | This module stores message bodies in a file system directory. 4 | 5 | ``` 6 | storage.blob.fs { 7 | root 8 | } 9 | ``` 10 | 11 | ``` 12 | storage.blob.fs 13 | ``` 14 | 15 | ## Configuration directives 16 | 17 | ### root _path_ 18 | Default: not set 19 | 20 | Path to the FS directory. Must be readable and writable by the server process. 21 | If it does not exist - it will be created (parent directory should be writable 22 | for this). Relative paths are interpreted relatively to server state directory. 23 | 24 | -------------------------------------------------------------------------------- /docs/reference/blob/s3.md: -------------------------------------------------------------------------------- 1 | # Amazon S3 2 | 3 | storage.blob.s3 module stores messages bodies in a bucket on S3-compatible storage. 4 | 5 | ``` 6 | storage.blob.s3 { 7 | endpoint play.min.io 8 | secure yes 9 | access_key "Q3AM3UQ867SPQQA43P2F" 10 | secret_key "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" 11 | bucket maddy-test 12 | 13 | # optional 14 | region eu-central-1 15 | object_prefix maddy/ 16 | creds access_key 17 | } 18 | ``` 19 | 20 | Example: 21 | 22 | ``` 23 | storage.imapsql local_mailboxes { 24 | ... 25 | msg_store s3 { 26 | endpoint s3.amazonaws.com 27 | access_key "..." 28 | secret_key "..." 29 | bucket maddy-messages 30 | region us-west-2 31 | creds access_key 32 | } 33 | } 34 | ``` 35 | 36 | ## Configuration directives 37 | 38 | ### endpoint _address:port_ 39 | 40 | **Required**. 41 | 42 | Root S3 endpoint. e.g. `s3.amazonaws.com` 43 | 44 | --- 45 | 46 | ### secure _boolean_ 47 | Default: `yes` 48 | 49 | Whether TLS should be used. 50 | 51 | --- 52 | 53 | ### access_key _string_
secret_key _string_ 54 | 55 | **Required**. 56 | 57 | Static S3 credentials. 58 | 59 | --- 60 | 61 | ### bucket _name_ 62 | 63 | **Required**. 64 | 65 | S3 bucket name. The bucket must exist and 66 | be read-writable. 67 | 68 | --- 69 | 70 | ### region _string_ 71 | Default: not set 72 | 73 | S3 bucket location. May be called "endpoint" in some manuals. 74 | 75 | --- 76 | 77 | ### object_prefix _string_ 78 | Default: empty string 79 | 80 | String to add to all keys stored by maddy. 81 | 82 | Can be useful when S3 is used as a file system. 83 | 84 | --- 85 | 86 | ### creds `access_key` | `file_minio` | `file_aws` | `iam` 87 | Default: `access_key` 88 | 89 | Credentials to use for accessing the S3 Bucket. 90 | 91 | Credential Types: 92 | 93 | - `access_key`: use AWS access key and secret access key 94 | - `file_minio`: use credentials for Minio present at ~/.mc/config.json 95 | - `file_aws`: use credentials for AWS S3 present at ~/.aws/credentials 96 | - `iam`: use AWS IAM instance profile for credentials. 97 | 98 | By default, access_key is used with the access key and secret access key present in the config. 99 | -------------------------------------------------------------------------------- /docs/reference/checks/actions.md: -------------------------------------------------------------------------------- 1 | # Check actions 2 | 3 | When a certain check module thinks the message is "bad", it takes some actions 4 | depending on its configuration. Most checks follow the same configuration 5 | structure and allow following actions to be taken on check failure: 6 | 7 | - Do nothing (`action ignore`) 8 | 9 | Useful for testing deployment of new checks. Check failures are still logged 10 | but they have no effect on message delivery. 11 | 12 | - Reject the message (`action reject`) 13 | 14 | Reject the message at connection time. No bounce is generated locally. 15 | 16 | - Quarantine the message (`action quarantine`) 17 | 18 | Mark message as 'quarantined'. If message is then delivered to the local 19 | storage, the storage backend can place the message in the 'Junk' mailbox. 20 | Another thing to keep in mind that 'target.remote' module 21 | will refuse to send quarantined messages. -------------------------------------------------------------------------------- /docs/reference/checks/dkim.md: -------------------------------------------------------------------------------- 1 | # DKIM 2 | 3 | This is the check module that performs verification of the DKIM signatures 4 | present on the incoming messages. 5 | 6 | ## Configuration directives 7 | 8 | ``` 9 | check.dkim { 10 | debug no 11 | required_fields From Subject 12 | allow_body_subset no 13 | no_sig_action ignore 14 | broken_sig_action ignore 15 | fail_open no 16 | } 17 | ``` 18 | 19 | ### debug _boolean_ 20 | Default: global directive value 21 | 22 | Log both successful and unsuccessful check executions instead of just 23 | unsuccessful. 24 | 25 | --- 26 | 27 | ### required_fields _string..._ 28 | Default: `From Subject` 29 | 30 | Header fields that should be included in each signature. If signature 31 | lacks any field listed in that directive, it will be considered invalid. 32 | 33 | Note that From is always required to be signed, even if it is not included in 34 | this directive. 35 | 36 | --- 37 | 38 | ### no_sig_action _action_ 39 | Default: `ignore` (recommended by RFC 6376) 40 | 41 | Action to take when message without any signature is received. 42 | 43 | Note that DMARC policy of the sender domain can request more strict handling of 44 | missing DKIM signatures. 45 | 46 | --- 47 | 48 | ### broken_sig_action _action_ 49 | Default: `ignore` (recommended by RFC 6376) 50 | 51 | Action to take when there are not valid signatures in a message. 52 | 53 | Note that DMARC policy of the sender domain can request more strict handling of 54 | broken DKIM signatures. 55 | 56 | --- 57 | 58 | ### fail_open _boolean_ 59 | Default: `no` 60 | 61 | Whether to accept the message if a temporary error occurs during DKIM 62 | verification. Rejecting the message with a 4xx code will require the sender 63 | to resend it later in a hope that the problem will be resolved. 64 | -------------------------------------------------------------------------------- /docs/reference/checks/milter.md: -------------------------------------------------------------------------------- 1 | # Milter client 2 | 3 | The 'milter' implements subset of Sendmail's milter protocol that can be used 4 | to integrate external software with maddy. 5 | maddy implements version 6 of the protocol, older versions are 6 | not supported. 7 | 8 | Notable limitations of protocol implementation in maddy include: 9 | 1. Changes of envelope sender address are not supported 10 | 2. Removal and addition of envelope recipients is not supported 11 | 3. Removal and replacement of header fields is not supported 12 | 4. Headers fields can be inserted only on top 13 | 5. Milter does not receive some "macros" provided by sendmail. 14 | 15 | Restrictions 1 and 2 are inherent to the maddy checks interface and cannot be 16 | removed without major changes to it. Restrictions 3, 4 and 5 are temporary due to 17 | incomplete implementation. 18 | 19 | ``` 20 | check.milter { 21 | endpoint 22 | fail_open false 23 | } 24 | 25 | milter 26 | ``` 27 | 28 | ## Arguments 29 | 30 | When defined inline, the first argument specifies endpoint to access milter 31 | via. See below. 32 | 33 | ## Configuration directives 34 | 35 | ### endpoint _scheme://path_ 36 | Default: not set 37 | 38 | Specifies milter protocol endpoint to use. 39 | The endpoit is specified in standard URL-like format: 40 | `tcp://127.0.0.1:6669` or `unix:///var/lib/milter/filter.sock` 41 | 42 | --- 43 | 44 | ### fail_open _boolean_ 45 | Default: `false` 46 | 47 | Toggles behavior on milter I/O errors. If false ("fail closed") - message is 48 | rejected with temporary error code. If true ("fail open") - check is skipped. 49 | 50 | -------------------------------------------------------------------------------- /docs/reference/checks/misc.md: -------------------------------------------------------------------------------- 1 | # Misc checks 2 | 3 | ## Configuration directives 4 | 5 | Following directives are defined for all modules listed below. 6 | 7 | ### fail_action `ignore` | `reject` | `quarantine` 8 | Default: `quarantine` 9 | 10 | Action to take when check fails. See [Check actions](../actions/) for details. 11 | 12 | --- 13 | 14 | ### debug _boolean_ 15 | Default: global directive value 16 | 17 | Log both successful and unsuccessful check executions instead of just 18 | unsuccessful. 19 | 20 | --- 21 | 22 | ### require_mx_record 23 | 24 | Check that domain in MAIL FROM command does have a MX record and none of them 25 | are "null" (contain a single dot as the host). 26 | 27 | By default, quarantines messages coming from servers missing MX records, 28 | use `fail_action` directive to change that. 29 | 30 | --- 31 | 32 | ### require_matching_rdns 33 | 34 | Check that source server IP does have a PTR record point to the domain 35 | specified in EHLO/HELO command. 36 | 37 | By default, quarantines messages coming from servers with mismatched or missing 38 | PTR record, use `fail_action` directive to change that. 39 | 40 | --- 41 | 42 | ### require_tls 43 | 44 | Check that the source server is connected via TLS; either directly, or by using 45 | the STARTTLS command. 46 | 47 | By default, rejects messages coming from unencrypted servers. Use the 48 | `fail_action` directive to change that. -------------------------------------------------------------------------------- /docs/reference/endpoints/openmetrics.md: -------------------------------------------------------------------------------- 1 | # OpenMetrics/Prometheus telemetry 2 | 3 | Various server statistics are provided in OpenMetrics format by the 4 | "openmetrics" module. 5 | 6 | To enable it, add the following line to the server config: 7 | 8 | ``` 9 | openmetrics tcp://127.0.0.1:9749 { } 10 | ``` 11 | 12 | Scrape endpoint would be `http://127.0.0.1:9749/metrics`. 13 | 14 | ## Metrics 15 | 16 | ``` 17 | # AUTH command failures due to invalid credentials. 18 | maddy_smtp_failed_logins{module} 19 | # Failed SMTP transaction commands (MAIL, RCPT, DATA). 20 | maddy_smtp_failed_commands{module, command, smtp_code, smtp_enchcode} 21 | # Messages rejected with 4xx code due to ratelimiting. 22 | maddy_smtp_ratelimit_deferred{module} 23 | # Amount of started SMTP transactions started. 24 | maddy_smtp_started_transactions{module} 25 | # Amount of aborted SMTP transactions started. 26 | maddy_smtp_aborted_transactions{module} 27 | # Amount of completed SMTP transactions. 28 | maddy_smtp_completed_transactions{module} 29 | # Number of times a check returned 'reject' result (may be more than processed 30 | # messages if check does so on per-recipient basis). 31 | maddy_check_reject{check} 32 | # Number of times a check returned 'quarantine' result (may be more than 33 | # processed messages if check does so on per-recipient basis). 34 | maddy_check_quarantined{check} 35 | # Amount of queued messages. 36 | maddy_queue_length{module, location} 37 | # Outbound connections established with specific TLS security level. 38 | maddy_remote_conns_tls_level{module, level} 39 | # Outbound connections established with specific MX security level. 40 | maddy_remote_conns_mx_level{module, level} 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/reference/modifiers/envelope.md: -------------------------------------------------------------------------------- 1 | # Envelope sender / recipient rewriting 2 | 3 | `replace_sender` and `replace_rcpt` modules replace SMTP envelope addresses 4 | based on the mapping defined by the table module (maddy-tables(5)). It is possible 5 | to specify 1:N mappings. This allows, for example, implementing mailing lists. 6 | 7 | The address is normalized before lookup (Punycode in domain-part is decoded, 8 | Unicode is normalized to NFC, the whole string is case-folded). 9 | 10 | First, the whole address is looked up. If there is no replacement, local-part 11 | of the address is looked up separately and is replaced in the address while 12 | keeping the domain part intact. Replacements are not applied recursively, that 13 | is, lookup is not repeated for the replacement. 14 | 15 | Recipients are not deduplicated after expansion, so message may be delivered 16 | multiple times to a single recipient. However, used delivery target can apply 17 | such deduplication (imapsql storage does it). 18 | 19 | Definition: 20 | 21 | ``` 22 | replace_rcpt
[table arguments] { 23 | [extended table config] 24 | } 25 | replace_sender
[table arguments] { 26 | [extended table config] 27 | } 28 | ``` 29 | 30 | Use examples: 31 | 32 | ``` 33 | modify { 34 | replace_rcpt file /etc/maddy/aliases 35 | replace_rcpt static { 36 | entry a@example.org b@example.org 37 | entry c@example.org c1@example.org c2@example.org 38 | } 39 | replace_rcpt regexp "(.+)@example.net" "$1@example.org" 40 | replace_rcpt regexp "(.+)@example.net" "$1@example.org" "$1@example.com" 41 | } 42 | ``` 43 | 44 | Possible contents of /etc/maddy/aliases in the example above: 45 | 46 | ``` 47 | # Replace 'cat' with any domain to 'dog'. 48 | # E.g. cat@example.net -> dog@example.net 49 | cat: dog 50 | 51 | # Replace cat@example.org with cat@example.com. 52 | # Takes priority over the previous line. 53 | cat@example.org: cat@example.com 54 | 55 | # Using aliases in multiple lines 56 | cat2: dog 57 | cat2: mouse 58 | cat2@example.org: cat@example.com 59 | cat2@example.org: cat@example.net 60 | # Comma-separated aliases in multiple lines 61 | cat3: dog , mouse 62 | cat3@example.org: cat@example.com , cat@example.net 63 | ``` -------------------------------------------------------------------------------- /docs/reference/table/auth.md: -------------------------------------------------------------------------------- 1 | # Authentication providers 2 | 3 | Most authentication providers are also usable as a table 4 | that contains all usernames known to the module. Exceptions are auth.external and 5 | pam as underlying interfaces do not define a way to check credentials 6 | existence. 7 | -------------------------------------------------------------------------------- /docs/reference/table/chain.md: -------------------------------------------------------------------------------- 1 | # Table chaining 2 | 3 | The table.chain module allows chaining together multiple table modules 4 | by using value returned by a previous table as an input for the second 5 | table. 6 | 7 | Example: 8 | ``` 9 | table.chain { 10 | step regexp "(.+)(\\+[^+"@]+)?@example.org" "$1@example.org" 11 | step file /etc/maddy/emails 12 | } 13 | ``` 14 | This will strip +prefix from mailbox before looking it up 15 | in /etc/maddy/emails list. 16 | 17 | ## Configuration directives 18 | 19 | ### step _table_ 20 | 21 | Adds a table module to the chain. If input value is not in the table 22 | (e.g. file) - return "not exists" error. 23 | 24 | --- 25 | 26 | ### optional_step _table_ 27 | 28 | Same as step but if input value is not in the table - it is passed to the 29 | next step without changes. 30 | 31 | Example: 32 | Something like this can be used to map emails to usernames 33 | after translating them via aliases map: 34 | 35 | ``` 36 | table.chain { 37 | optional_step file /etc/maddy/aliases 38 | step regexp "(.+)@(.+)" "$1" 39 | } 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /docs/reference/table/email_localpart.md: -------------------------------------------------------------------------------- 1 | # Email local part 2 | 3 | The module `table.email_localpart` extracts and unescapes local ("username") part 4 | of the email address. 5 | 6 | E.g. 7 | 8 | * `test@example.org` => `test` 9 | * `"test @ a"@example.org` => `test @ a` 10 | 11 | Mappings for invalid emails are not defined (will be treated as non-existing 12 | values). 13 | 14 | ``` 15 | table.email_localpart { } 16 | ``` 17 | 18 | `table.email_localpart_optional` works the same, but returns non-email strings 19 | as is. This can be used if you want to accept both `user@example.org` and 20 | `user` somewhere and treat it the same. 21 | -------------------------------------------------------------------------------- /docs/reference/table/email_with_domain.md: -------------------------------------------------------------------------------- 1 | # Email with domain 2 | 3 | The table module `table.email_with_domain` appends one or more 4 | domains (allowing 1:N expansion) to the specified value. 5 | 6 | ``` 7 | table.email_with_domain DOMAIN DOMAIN... { } 8 | ``` 9 | 10 | It can be used to implement domain-level expansion for aliases if used together 11 | with `table.chain`. Example: 12 | 13 | ``` 14 | modify { 15 | replace_rcpt chain { 16 | step email_local_part 17 | step email_with_domain example.org example.com 18 | } 19 | } 20 | ``` 21 | 22 | This configuration will alias `anything@anydomain` to `anything@example.org` 23 | and `anything@example.com`. 24 | 25 | It is also useful with `authorize_sender` to authorize sending using multiple 26 | addresses under different domains if non-email usernames are used for 27 | authentication: 28 | 29 | ``` 30 | check.authorize_sender { 31 | ... 32 | user_to_email email_with_domain example.org example.com 33 | } 34 | ``` 35 | 36 | This way, user authenticated as `user` will be allowed to use 37 | `user@example.org` or `user@example.com` as a sender address. 38 | -------------------------------------------------------------------------------- /docs/reference/table/file.md: -------------------------------------------------------------------------------- 1 | # File 2 | 3 | table.file module builds string-string mapping from a text file. 4 | 5 | File is reloaded every 15 seconds if there are any changes (detected using 6 | modification time). No changes are applied if file contains syntax errors. 7 | 8 | Definition: 9 | ``` 10 | file 11 | ``` 12 | or 13 | ``` 14 | file { 15 | file 16 | } 17 | ``` 18 | 19 | Usage example: 20 | ``` 21 | # Resolve SMTP address aliases using text file mapping. 22 | modify { 23 | replace_rcpt file /etc/maddy/aliases 24 | } 25 | ``` 26 | 27 | ## Syntax 28 | 29 | Better demonstrated by examples: 30 | 31 | ``` 32 | # Lines starting with # are ignored. 33 | 34 | # And so are lines only with whitespace. 35 | 36 | # Whenever 'aaa' is looked up, return 'bbb' 37 | aaa: bbb 38 | 39 | # Trailing and leading whitespace is ignored. 40 | ccc: ddd 41 | 42 | # If there is no colon, the string is translated into "" 43 | # That is, the following line is equivalent to 44 | # aaa: 45 | aaa 46 | 47 | # If the same key is used multiple times - table.file will return 48 | # multiple values when queries. 49 | ddd: firstvalue 50 | ddd: secondvalue 51 | 52 | # Alternatively, multiple values can be specified 53 | # using a comma. There is no support for escaping 54 | # so you would have to use a different format if you require 55 | # comma-separated values. 56 | ddd: firstvalue, secondvalue 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /docs/reference/table/regexp.md: -------------------------------------------------------------------------------- 1 | # Regexp rewrite table 2 | 3 | The 'regexp' module implements table lookups by applying a regular expression 4 | to the key value. If it matches - 'replacement' value is returned with $N 5 | placeholders being replaced with corresponding capture groups from the match. 6 | Otherwise, no value is returned. 7 | 8 | The regular expression syntax is the subset of PCRE. See 9 | [https://golang.org/pkg/regexp/syntax](https://golang.org/pkg/regexp/syntax)/ for details. 10 | 11 | ``` 12 | table.regexp [replacement] { 13 | full_match yes 14 | case_insensitive yes 15 | expand_placeholders yes 16 | } 17 | ``` 18 | 19 | Note that [replacement] is optional. If it is not included - table.regexp 20 | will return the original string, therefore acting as a regexp match check. 21 | This can be useful in combination in `destination_in` for 22 | advanced matching: 23 | 24 | ``` 25 | destination_in regexp ".*-bounce+.*@example.com" { 26 | ... 27 | } 28 | ``` 29 | 30 | ## Configuration directives 31 | 32 | ### full_match _boolean_ 33 | Default: `yes` 34 | 35 | Whether to implicitly add start/end anchors to the regular expression. 36 | That is, if `full_match` is `yes`, then the provided regular expression should 37 | match the whole string. With `no` - partial match is enough. 38 | 39 | --- 40 | 41 | ### case_insensitive _boolean_ 42 | Default: `yes` 43 | 44 | Whether to make matching case-insensitive. 45 | 46 | --- 47 | 48 | ### expand_placeholders _boolean_ 49 | Default: `yes` 50 | 51 | Replace '$name' and '${name}' in the replacement string with contents of 52 | corresponding capture groups from the match. 53 | 54 | To insert a literal $ in the output, use $$ in the template. 55 | 56 | ## Identity table (table.identity) 57 | 58 | The module 'identity' is a table module that just returns the key looked up. 59 | 60 | ``` 61 | table.identity { } 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /docs/reference/table/static.md: -------------------------------------------------------------------------------- 1 | # Static table 2 | 3 | The 'static' module implements table lookups using key-value pairs in its 4 | configuration. 5 | 6 | ``` 7 | table.static { 8 | entry KEY1 VALUE1 9 | entry KEY2 VALUE2 10 | ... 11 | } 12 | ``` 13 | 14 | ## Configuration directives 15 | 16 | ### entry _key_ _value_ 17 | 18 | Add an entry to the table. 19 | 20 | If the same key is used multiple times, the last one takes effect. 21 | 22 | -------------------------------------------------------------------------------- /docs/third-party/rspamd.md: -------------------------------------------------------------------------------- 1 | # rspamd 2 | 3 | maddy has direct support for rspamd HTTP protocol. There is no need to use 4 | milter proxy. 5 | 6 | If rspamd is running locally, it is enough to just add `rspamd` check 7 | with default configuration into appropriate check block (probably in 8 | local_routing): 9 | ``` 10 | check { 11 | ... 12 | rspamd 13 | } 14 | ``` 15 | 16 | You might want to disable builtin SPF, DKIM and DMARC for performance 17 | reasons but note that at the moment, maddy will not generate 18 | Authentication-Results field with rspamd results. 19 | 20 | If rspamd is not running on a local machine, change api_path to point 21 | to the "normal" worker socket: 22 | 23 | ``` 24 | check { 25 | ... 26 | rspamd { 27 | api_path http://spam-check.example.org:11333 28 | } 29 | } 30 | ``` 31 | 32 | Default mapping of rspamd action -> maddy action is as follows: 33 | 34 | - "add header" => Quarantine 35 | - "rewrite subject" => Quarantine 36 | - "soft reject" => Reject with temporary error 37 | - "reject" => Reject with permanent error 38 | - "greylist" => Ignored -------------------------------------------------------------------------------- /docs/third-party/smtp-servers.md: -------------------------------------------------------------------------------- 1 | # External SMTP server 2 | 3 | It is possible to use maddy as an IMAP server only and have it interface with 4 | external SMTP server using standard protocols. 5 | 6 | Here is the minimal configuration that creates a local IMAP index, credentials 7 | database and IMAP endpoint: 8 | ``` 9 | # Credentials DB. 10 | table.pass_table local_authdb { 11 | table sql_table { 12 | driver sqlite3 13 | dsn credentials.db 14 | table_name passwords 15 | } 16 | } 17 | 18 | # IMAP storage/index. 19 | storage.imapsql local_mailboxes { 20 | driver sqlite3 21 | dsn imapsql.db 22 | } 23 | 24 | # IMAP endpoint using these above. 25 | imap tls://0.0.0.0:993 tcp://0.0.0.0:143 { 26 | auth &local_authdb 27 | storage &local_mailboxes 28 | } 29 | ``` 30 | 31 | To accept local messages from an external SMTP server 32 | it is possible to create an LMTP endpoint: 33 | ``` 34 | # LMTP endpoint on Unix socket delivering to IMAP storage 35 | # in previous config snippet. 36 | lmtp unix:/run/maddy/lmtp.sock { 37 | hostname mx.maddy.test 38 | 39 | deliver_to &local_mailboxes 40 | } 41 | ``` 42 | 43 | Look up documentation for your SMTP server on how to make it 44 | send messages using LMTP to /run/maddy/lmtp.sock. 45 | 46 | To handle authentication for Submission (client-server SMTP) SMTP server 47 | needs to access credentials database used by maddy. maddy implements 48 | server side of Dovecot authentication protocol so you can use 49 | it if SMTP server implements "Dovecot SASL" client. 50 | 51 | To create a Dovecot-compatible sasld endpoint, add the following configuration 52 | block: 53 | ``` 54 | # Dovecot-compatible sasld endpoint using data from local_authdb. 55 | dovecot_sasld unix:/run/maddy/auth-client.sock { 56 | auth &local_authdb 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/tutorials/building-from-source.md: -------------------------------------------------------------------------------- 1 | # Building from source 2 | 3 | ## System dependencies 4 | 5 | You need C toolchain, Go toolchain and Make: 6 | 7 | On Debian-based system this should work: 8 | ``` 9 | apt-get install golang-1.23 gcc libc6-dev make 10 | ``` 11 | 12 | Additionally, if you want manual pages, you should also have scdoc installed. 13 | Figuring out the appropriate way to get scdoc is left as an exercise for 14 | reader (for Ubuntu 22.04 LTS it is in repositories). 15 | 16 | ## Recent Go toolchain 17 | 18 | maddy depends on a rather recent Go toolchain version that may not be 19 | available in some distributions (*cough* Debian *cough*). 20 | 21 | `go` command in Go 1.21 or newer will automatically download up-to-date 22 | toolchain to build maddy. It is necessary to run commands below only 23 | if you have `go` command version older than 1.21. 24 | 25 | ``` 26 | wget "https://go.dev/dl/go1.23.5.linux-amd64.tar.gz" 27 | tar xf "go1.23.5.linux-amd64.tar.gz" 28 | export GOROOT="$PWD/go" 29 | export PATH="$PWD/go/bin:$PATH" 30 | ``` 31 | 32 | ## Step-by-step 33 | 34 | 1. Clone repository 35 | ``` 36 | $ git clone https://github.com/foxcpp/maddy.git 37 | $ cd maddy 38 | ``` 39 | 40 | 2. Select the appropriate version to build: 41 | ``` 42 | $ git checkout v0.8.0 # a specific release 43 | $ git checkout master # next bugfix release 44 | $ git checkout dev # next feature release 45 | ``` 46 | 47 | 3. Build & install it 48 | ``` 49 | $ ./build.sh 50 | $ sudo ./build.sh install 51 | ``` 52 | 53 | 4. Finish setup as described in [Setting up](../setting-up) (starting from System configuration). 54 | 55 | 56 | -------------------------------------------------------------------------------- /framework/address/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | // Package address provides utilities for parsing 20 | // and validation of RFC 2821 addresses. 21 | package address 22 | -------------------------------------------------------------------------------- /framework/address/rfc6531_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package address 20 | 21 | import ( 22 | "strings" 23 | "testing" 24 | ) 25 | 26 | func TestToASCII(t *testing.T) { 27 | test := addrFuncTest(t, ToASCII) 28 | test("test@тест.example.org", "test@xn--e1aybc.example.org", false) 29 | test("test@org."+strings.Repeat("x", 65535)+"\uFF00", "test@org."+strings.Repeat("x", 65535)+"\uFF00", true) 30 | test("тест@example.org", "тест@example.org", true) 31 | test("postmaster", "postmaster", false) 32 | test("postmaster@", "postmaster@", true) 33 | } 34 | 35 | func TestToUnicode(t *testing.T) { 36 | test := addrFuncTest(t, ToUnicode) 37 | test("test@xn--e1aybc.example.org", "test@тест.example.org", false) 38 | test("test@xn--9999999999999999999a.org", "test@xn--9999999999999999999a.org", true) 39 | test("postmaster", "postmaster", false) 40 | test("postmaster@", "postmaster@", true) 41 | } 42 | -------------------------------------------------------------------------------- /framework/address/validation_test.go: -------------------------------------------------------------------------------- 1 | package address_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/foxcpp/maddy/framework/address" 8 | ) 9 | 10 | func TestValidMailboxName(t *testing.T) { 11 | if !address.ValidMailboxName("caddy.bug") { 12 | t.Error("caddy.bug should be valid mailbox name") 13 | } 14 | } 15 | 16 | func TestValidDomain(t *testing.T) { 17 | for _, c := range []struct { 18 | Domain string 19 | Valid bool 20 | }{ 21 | {Domain: "maddy.email", Valid: true}, 22 | {Domain: "", Valid: false}, 23 | {Domain: "maddy.email.", Valid: true}, 24 | {Domain: "..", Valid: false}, 25 | {Domain: strings.Repeat("a", 256), Valid: false}, 26 | {Domain: "äõäoaõoäaõaäõaoäaoaäõoaäooaoaoiuaiauäõiuüõaõäiauõaaa.tld", Valid: true}, // https://github.com/foxcpp/maddy/issues/554 27 | {Domain: "xn--oaoaaaoaoaoaooaoaoiuaiauiuaiauaaa-f1cadccdcmd01eddchqcbe07a.tld", Valid: true}, // https://github.com/foxcpp/maddy/issues/554 28 | } { 29 | if actual := address.ValidDomain(c.Domain); actual != c.Valid { 30 | t.Errorf("expected domain %v to be valid=%v, but got %v", c.Domain, c.Valid, actual) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /framework/buffer/bytesreader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package buffer 20 | 21 | import ( 22 | "bytes" 23 | ) 24 | 25 | // BytesReader is a wrapper for bytes.Reader that stores the original []byte 26 | // value and allows to retrieve it. 27 | // 28 | // It is meant for passing to libraries that expect a io.Reader 29 | // but apply certain optimizations when the Reader implements 30 | // Bytes() interface. 31 | type BytesReader struct { 32 | *bytes.Reader 33 | value []byte 34 | } 35 | 36 | // Bytes returns the unread portion of underlying slice used to construct 37 | // BytesReader. 38 | func (br BytesReader) Bytes() []byte { 39 | return br.value[int(br.Size())-br.Len():] 40 | } 41 | 42 | // Copy returns the BytesReader reading from the same slice as br at the same 43 | // position. 44 | func (br BytesReader) Copy() BytesReader { 45 | return NewBytesReader(br.Bytes()) 46 | } 47 | 48 | // Close is a dummy method for implementation of io.Closer so BytesReader can 49 | // be used in MemoryBuffer directly. 50 | func (br BytesReader) Close() error { 51 | return nil 52 | } 53 | 54 | func NewBytesReader(b []byte) BytesReader { 55 | // BytesReader and not *BytesReader because BytesReader already wraps two 56 | // pointers and double indirection would be pointless. 57 | return BytesReader{ 58 | Reader: bytes.NewReader(b), 59 | value: b, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /framework/buffer/memory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package buffer 20 | 21 | import ( 22 | "io" 23 | ) 24 | 25 | // MemoryBuffer implements Buffer interface using byte slice. 26 | type MemoryBuffer struct { 27 | Slice []byte 28 | } 29 | 30 | func (mb MemoryBuffer) Open() (io.ReadCloser, error) { 31 | return NewBytesReader(mb.Slice), nil 32 | } 33 | 34 | func (mb MemoryBuffer) Len() int { 35 | return len(mb.Slice) 36 | } 37 | 38 | func (mb MemoryBuffer) Remove() error { 39 | return nil 40 | } 41 | 42 | // BufferInMemory is a convenience function which creates MemoryBuffer with 43 | // contents of the passed io.Reader. 44 | func BufferInMemory(r io.Reader) (Buffer, error) { 45 | blob, err := io.ReadAll(r) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return MemoryBuffer{Slice: blob}, nil 50 | } 51 | -------------------------------------------------------------------------------- /framework/cfgparser/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package parser 20 | 21 | import ( 22 | "os" 23 | "regexp" 24 | "strings" 25 | ) 26 | 27 | func expandEnvironment(nodes []Node) []Node { 28 | // If nodes is nil - don't replace with empty slice, as nil indicates "no 29 | // block". 30 | if nodes == nil { 31 | return nil 32 | } 33 | 34 | replacer := buildEnvReplacer() 35 | newNodes := make([]Node, 0, len(nodes)) 36 | for _, node := range nodes { 37 | node.Name = removeUnexpandedEnvvars(replacer.Replace(node.Name)) 38 | newArgs := make([]string, 0, len(node.Args)) 39 | for _, arg := range node.Args { 40 | newArgs = append(newArgs, removeUnexpandedEnvvars(replacer.Replace(arg))) 41 | } 42 | node.Args = newArgs 43 | node.Children = expandEnvironment(node.Children) 44 | newNodes = append(newNodes, node) 45 | } 46 | return newNodes 47 | } 48 | 49 | var unixEnvvarRe = regexp.MustCompile(`{env:([^\$]+)}`) 50 | 51 | func removeUnexpandedEnvvars(s string) string { 52 | s = unixEnvvarRe.ReplaceAllString(s, "") 53 | return s 54 | } 55 | 56 | func buildEnvReplacer() *strings.Replacer { 57 | env := os.Environ() 58 | pairs := make([]string, 0, len(env)*4) 59 | for _, entry := range env { 60 | parts := strings.SplitN(entry, "=", 2) 61 | key := parts[0] 62 | value := parts[1] 63 | 64 | pairs = append(pairs, "{env:"+key+"}", value) 65 | } 66 | return strings.NewReplacer(pairs...) 67 | } 68 | -------------------------------------------------------------------------------- /framework/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package config 20 | 21 | import ( 22 | "fmt" 23 | 24 | parser "github.com/foxcpp/maddy/framework/cfgparser" 25 | ) 26 | 27 | type ( 28 | Node = parser.Node 29 | ) 30 | 31 | func NodeErr(node Node, f string, args ...interface{}) error { 32 | if node.File == "" { 33 | return fmt.Errorf(f, args...) 34 | } 35 | return fmt.Errorf("%s:%d: %s", node.File, node.Line, fmt.Sprintf(f, args...)) 36 | } 37 | -------------------------------------------------------------------------------- /framework/config/directories.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package config 20 | 21 | var ( 22 | // StateDirectory contains the path to the directory that 23 | // should be used to store any data that should be 24 | // preserved between sessions. 25 | // 26 | // Value of this variable must not change after initialization 27 | // in cmd/maddy/main.go. 28 | StateDirectory string 29 | 30 | // RuntimeDirectory contains the path to the directory that 31 | // should be used to store any temporary data. 32 | // 33 | // It should be preferred over os.TempDir, which is 34 | // global and world-readable on most systems, while 35 | // RuntimeDirectory can be dedicated for maddy. 36 | // 37 | // Value of this variable must not change after initialization 38 | // in cmd/maddy/main.go. 39 | RuntimeDirectory string 40 | 41 | // LibexecDirectory contains the path to the directory 42 | // where helper binaries should be searched. 43 | // 44 | // Value of this variable must not change after initialization 45 | // in cmd/maddy/main.go. 46 | LibexecDirectory string 47 | ) 48 | -------------------------------------------------------------------------------- /framework/config/lexer/README.md: -------------------------------------------------------------------------------- 1 | caddyfile lexer copied from [caddy](https://github.com/caddyserver/caddy) project. 2 | 3 | Taken from the following commit: 4 | ``` 5 | commit ed4c2775e46b924d4851e04cc281633b1b2c15af 6 | Author: Alexander Danilov 7 | Date: Wed Aug 21 20:13:34 2019 +0300 8 | 9 | main: log caddy version on start (#2717) 10 | 11 | ``` 12 | 13 | License of the original code is included in LICENSE.APACHE file in this 14 | directory. 15 | 16 | No signficant changes was made to the code (e.g. it is safe to update it from 17 | caddy repo). 18 | 19 | The code is copied because caddy brings quite a lot of dependencies we don't 20 | use and this slows down many tools. 21 | -------------------------------------------------------------------------------- /framework/config/lexer/parse.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package lexer 20 | 21 | import ( 22 | "io" 23 | ) 24 | 25 | // allTokens lexes the entire input, but does not parse it. 26 | // It returns all the tokens from the input, unstructured 27 | // and in order. 28 | func allTokens(input io.Reader) ([]Token, error) { 29 | l := new(lexer) 30 | err := l.load(input) 31 | if err != nil { 32 | return nil, err 33 | } 34 | var tokens []Token 35 | for l.next() { 36 | tokens = append(tokens, l.token) 37 | } 38 | if err := l.err(); err != nil { 39 | return nil, err 40 | } 41 | return tokens, nil 42 | } 43 | -------------------------------------------------------------------------------- /framework/dns/debugflags.go: -------------------------------------------------------------------------------- 1 | //go:build debugflags 2 | // +build debugflags 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package dns 23 | 24 | import ( 25 | maddycli "github.com/foxcpp/maddy/internal/cli" 26 | "github.com/urfave/cli/v2" 27 | ) 28 | 29 | func init() { 30 | maddycli.AddGlobalFlag(&cli.StringFlag{ 31 | Name: "debug.dnsoverride", 32 | Usage: "replace the DNS resolver address", 33 | Value: "system-default", 34 | Destination: &overrideServ, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /framework/dns/idna.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package dns 20 | 21 | import ( 22 | "golang.org/x/net/idna" 23 | "golang.org/x/text/unicode/norm" 24 | ) 25 | 26 | // SelectIDNA is a convenience function for encoding to/from Punycode. 27 | // 28 | // If ulabel is true, it returns U-label encoded domain in the Unicode NFC 29 | // form. 30 | // If ulabel is false, it returns A-label encoded domain. 31 | func SelectIDNA(ulabel bool, domain string) (string, error) { 32 | if ulabel { 33 | uDomain, err := idna.ToUnicode(domain) 34 | return norm.NFC.String(uDomain), err 35 | } 36 | return idna.ToASCII(domain) 37 | } 38 | -------------------------------------------------------------------------------- /framework/dns/override.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package dns 20 | 21 | import ( 22 | "context" 23 | "net" 24 | "time" 25 | ) 26 | 27 | var overrideServ string 28 | 29 | // override globally overrides the used DNS server address with one provided. 30 | // This function is meant only for testing. It should be called before any modules are 31 | // initialized to have full effect. 32 | // 33 | // The server argument is in form of "IP:PORT". It is expected that the server 34 | // will be available both using TCP and UDP on the same port. 35 | func override(server string) { 36 | net.DefaultResolver.PreferGo = true 37 | net.DefaultResolver.Dial = func(ctx context.Context, network, _ string) (net.Conn, error) { 38 | dialer := net.Dialer{ 39 | // This is localhost, it is either running or not. Fail quickly if 40 | // we can't connect. 41 | Timeout: 1 * time.Second, 42 | } 43 | 44 | switch network { 45 | case "udp", "udp4", "udp6": 46 | return dialer.DialContext(ctx, "udp4", server) 47 | case "tcp", "tcp4", "tcp6": 48 | return dialer.DialContext(ctx, "tcp4", server) 49 | default: 50 | panic("OverrideDNS.Dial: unknown network") 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /framework/exterrors/dns.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package exterrors 20 | 21 | import ( 22 | "net" 23 | ) 24 | 25 | func UnwrapDNSErr(err error) (reason string, misc map[string]interface{}) { 26 | dnsErr, ok := err.(*net.DNSError) 27 | if !ok { 28 | // Return non-nil in case the user will try to 'extend' it with its own 29 | // values. 30 | return "", map[string]interface{}{} 31 | } 32 | 33 | // Nor server name, nor DNS name are usually useful, so exclude them. 34 | return dnsErr.Err, map[string]interface{}{} 35 | } 36 | -------------------------------------------------------------------------------- /framework/exterrors/exterrors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | // Package errors defines error-handling and primitives 20 | // used across maddy, notably to pass additional error 21 | // information across module boundaries. 22 | package exterrors 23 | -------------------------------------------------------------------------------- /framework/exterrors/fields.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package exterrors 20 | 21 | type fieldsErr interface { 22 | Fields() map[string]interface{} 23 | } 24 | 25 | type unwrapper interface { 26 | Unwrap() error 27 | } 28 | 29 | type fieldsWrap struct { 30 | err error 31 | fields map[string]interface{} 32 | } 33 | 34 | func (fw fieldsWrap) Error() string { 35 | return fw.err.Error() 36 | } 37 | 38 | func (fw fieldsWrap) Unwrap() error { 39 | return fw.err 40 | } 41 | 42 | func (fw fieldsWrap) Fields() map[string]interface{} { 43 | return fw.fields 44 | } 45 | 46 | func Fields(err error) map[string]interface{} { 47 | fields := make(map[string]interface{}, 5) 48 | 49 | for err != nil { 50 | errFields, ok := err.(fieldsErr) 51 | if ok { 52 | for k, v := range errFields.Fields() { 53 | // Outer errors override fields of the inner ones. 54 | // Not the reverse. 55 | if fields[k] != nil { 56 | continue 57 | } 58 | fields[k] = v 59 | } 60 | } 61 | 62 | unwrap, ok := err.(unwrapper) 63 | if !ok { 64 | break 65 | } 66 | err = unwrap.Unwrap() 67 | } 68 | 69 | return fields 70 | } 71 | 72 | func WithFields(err error, fields map[string]interface{}) error { 73 | return fieldsWrap{err: err, fields: fields} 74 | } 75 | -------------------------------------------------------------------------------- /framework/future/future_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package future 20 | 21 | import ( 22 | "context" 23 | "errors" 24 | "testing" 25 | "time" 26 | ) 27 | 28 | func TestFuture_SetBeforeGet(t *testing.T) { 29 | f := New() 30 | 31 | f.Set(1, errors.New("1")) 32 | val, err := f.Get() 33 | if err.Error() != "1" { 34 | t.Error("Wrong error:", err) 35 | } 36 | 37 | if val, _ := val.(int); val != 1 { 38 | t.Fatal("wrong val received from Get") 39 | } 40 | } 41 | 42 | func TestFuture_Wait(t *testing.T) { 43 | f := New() 44 | 45 | go func() { 46 | time.Sleep(500 * time.Millisecond) 47 | f.Set(1, errors.New("1")) 48 | }() 49 | 50 | val, err := f.Get() 51 | if val, _ := val.(int); val != 1 { 52 | t.Fatal("wrong val received from Get") 53 | } 54 | if err.Error() != "1" { 55 | t.Error("Wrong error:", err) 56 | } 57 | 58 | val, err = f.Get() 59 | if val, _ := val.(int); val != 1 { 60 | t.Fatal("wrong val received from Get on second try") 61 | } 62 | if err.Error() != "1" { 63 | t.Error("Wrong error:", err) 64 | } 65 | } 66 | 67 | func TestFuture_WaitCtx(t *testing.T) { 68 | f := New() 69 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 70 | defer cancel() 71 | _, err := f.GetContext(ctx) 72 | if !errors.Is(err, context.DeadlineExceeded) { 73 | t.Fatal("context is not cancelled") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /framework/log/output.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package log 20 | 21 | import ( 22 | "time" 23 | ) 24 | 25 | type Output interface { 26 | Write(stamp time.Time, debug bool, msg string) 27 | Close() error 28 | } 29 | 30 | type multiOut struct { 31 | outs []Output 32 | } 33 | 34 | func (m multiOut) Write(stamp time.Time, debug bool, msg string) { 35 | for _, out := range m.outs { 36 | out.Write(stamp, debug, msg) 37 | } 38 | } 39 | 40 | func (m multiOut) Close() error { 41 | for _, out := range m.outs { 42 | if err := out.Close(); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func MultiOutput(outputs ...Output) Output { 50 | return multiOut{outputs} 51 | } 52 | 53 | type funcOut struct { 54 | out func(time.Time, bool, string) 55 | close func() error 56 | } 57 | 58 | func (f funcOut) Write(stamp time.Time, debug bool, msg string) { 59 | f.out(stamp, debug, msg) 60 | } 61 | 62 | func (f funcOut) Close() error { 63 | return f.close() 64 | } 65 | 66 | func FuncOutput(f func(time.Time, bool, string), close func() error) Output { 67 | return funcOut{f, close} 68 | } 69 | 70 | type NopOutput struct{} 71 | 72 | func (NopOutput) Write(time.Time, bool, string) {} 73 | 74 | func (NopOutput) Close() error { return nil } 75 | -------------------------------------------------------------------------------- /framework/log/syslog.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !plan9 2 | // +build !windows,!plan9 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package log 23 | 24 | import ( 25 | "fmt" 26 | "log/syslog" 27 | "os" 28 | "time" 29 | ) 30 | 31 | type syslogOut struct { 32 | w *syslog.Writer 33 | } 34 | 35 | func (s syslogOut) Write(stamp time.Time, debug bool, msg string) { 36 | var err error 37 | if debug { 38 | err = s.w.Debug(msg + "\n") 39 | } else { 40 | err = s.w.Info(msg + "\n") 41 | } 42 | 43 | if err != nil { 44 | fmt.Fprintf(os.Stderr, "!!! Failed to send message to syslog daemon: %v\n", err) 45 | } 46 | } 47 | 48 | func (s syslogOut) Close() error { 49 | return s.w.Close() 50 | } 51 | 52 | // SyslogOutput returns a log.Output implementation that will send 53 | // messages to the system syslog daemon. 54 | // 55 | // Regular messages will be written with INFO priority, 56 | // debug messages will be written with DEBUG priority. 57 | // 58 | // Returned log.Output object is goroutine-safe. 59 | func SyslogOutput() (Output, error) { 60 | w, err := syslog.New(syslog.LOG_MAIL|syslog.LOG_INFO, "maddy") 61 | return syslogOut{w}, err 62 | } 63 | -------------------------------------------------------------------------------- /framework/log/syslog_stub.go: -------------------------------------------------------------------------------- 1 | //go:build windows || plan9 2 | // +build windows plan9 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package log 23 | 24 | import ( 25 | "errors" 26 | ) 27 | 28 | // SyslogOutput returns a log.Output implementation that will send 29 | // messages to the system syslog daemon. 30 | // 31 | // Regular messages will be written with INFO priority, 32 | // debug messages will be written with DEBUG priority. 33 | // 34 | // Returned log.Output object is goroutine-safe. 35 | func SyslogOutput() (Output, error) { 36 | return nil, errors.New("log: syslog output is not supported on windows") 37 | } 38 | -------------------------------------------------------------------------------- /framework/log/zap.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "go.uber.org/zap/zapcore" 5 | ) 6 | 7 | // TODO: Migrate to using actual zapcore to improve logging performance 8 | 9 | type zapLogger struct { 10 | L Logger 11 | } 12 | 13 | func (l zapLogger) Enabled(level zapcore.Level) bool { 14 | if l.L.Debug { 15 | return true 16 | } 17 | return level > zapcore.DebugLevel 18 | } 19 | 20 | func (l zapLogger) With(fields []zapcore.Field) zapcore.Core { 21 | enc := zapcore.NewMapObjectEncoder() 22 | for _, f := range fields { 23 | f.AddTo(enc) 24 | } 25 | newF := make(map[string]interface{}, len(l.L.Fields)+len(enc.Fields)) 26 | for k, v := range l.L.Fields { 27 | newF[k] = v 28 | } 29 | for k, v := range enc.Fields { 30 | newF[k] = v 31 | } 32 | l.L.Fields = newF 33 | return l 34 | } 35 | 36 | func (l zapLogger) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { 37 | if l.Enabled(entry.Level) { 38 | return ce.AddCore(entry, l) 39 | } 40 | return ce 41 | } 42 | 43 | func (l zapLogger) Write(entry zapcore.Entry, fields []zapcore.Field) error { 44 | enc := zapcore.NewMapObjectEncoder() 45 | for _, f := range fields { 46 | f.AddTo(enc) 47 | } 48 | if entry.LoggerName != "" { 49 | l.L.Name += "/" + entry.LoggerName 50 | } 51 | l.L.log(entry.Level == zapcore.DebugLevel, l.L.formatMsg(entry.Message, enc.Fields)) 52 | return nil 53 | } 54 | 55 | func (zapLogger) Sync() error { 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /framework/module/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package module 20 | 21 | import "errors" 22 | 23 | // ErrUnknownCredentials should be returned by auth. provider if supplied 24 | // credentials are valid for it but are not recognized (e.g. not found in 25 | // used DB). 26 | var ErrUnknownCredentials = errors.New("unknown credentials") 27 | 28 | // PlainAuth is the interface implemented by modules providing authentication using 29 | // username:password pairs. 30 | // 31 | // Modules implementing this interface should be registered with "auth." prefix in name. 32 | type PlainAuth interface { 33 | AuthPlain(username, password string) error 34 | } 35 | 36 | // PlainUserDB is a local credentials store that can be managed using maddy command 37 | // utility. 38 | type PlainUserDB interface { 39 | PlainAuth 40 | ListUsers() ([]string, error) 41 | CreateUser(username, password string) error 42 | SetUserPassword(username, password string) error 43 | DeleteUser(username string) error 44 | } 45 | -------------------------------------------------------------------------------- /framework/module/blob_store.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | type Blob interface { 10 | Sync() error 11 | io.Writer 12 | io.Closer 13 | } 14 | 15 | var ErrNoSuchBlob = errors.New("blob_store: no such object") 16 | 17 | const UnknownBlobSize int64 = -1 18 | 19 | // BlobStore is the interface used by modules providing large binary object 20 | // storage. 21 | type BlobStore interface { 22 | // Create creates a new blob for writing. 23 | // 24 | // Sync will be called on the returned Blob object after -all- data has 25 | // been successfully written. 26 | // 27 | // Close without Sync can be assumed to happen due to an unrelated error 28 | // and stored data can be discarded. 29 | // 30 | // blobSize indicates the exact amount of bytes that will be written 31 | // If -1 is passed - it is unknown and implementation will not make 32 | // any assumptions about the blob size. Error can be returned by any 33 | // Blob method if more than than blobSize bytes get written. 34 | // 35 | // Passed context will cover the entire blob write operation. 36 | Create(ctx context.Context, key string, blobSize int64) (Blob, error) 37 | 38 | // Open returns the reader for the object specified by 39 | // passed key. 40 | // 41 | // If no such object exists - ErrNoSuchBlob is returned. 42 | Open(ctx context.Context, key string) (io.ReadCloser, error) 43 | 44 | // Delete removes a set of keys from store. Non-existent keys are ignored. 45 | Delete(ctx context.Context, keys []string) error 46 | } 47 | -------------------------------------------------------------------------------- /framework/module/imap_filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package module 20 | 21 | import ( 22 | "github.com/emersion/go-message/textproto" 23 | "github.com/foxcpp/maddy/framework/buffer" 24 | ) 25 | 26 | // IMAPFilter is interface used by modules that want to modify IMAP-specific message 27 | // attributes on delivery. 28 | // 29 | // Modules implementing this interface should be registered with namespace prefix 30 | // "imap.filter". 31 | type IMAPFilter interface { 32 | // IMAPFilter is called when message is about to be stored in IMAP-compatible 33 | // storage. It is called only for messages delivered over SMTP, hdr and body 34 | // contain the message exactly how it will be stored. 35 | // 36 | // Filter can change the target directory by returning non-empty folder value. 37 | // Additionally it can add additional IMAP flags to the message by returning 38 | // them. 39 | // 40 | // Errors returned by IMAPFilter will be just logged and will not cause delivery 41 | // to fail. 42 | IMAPFilter(accountName string, rcptTo string, meta *MsgMetadata, hdr textproto.Header, body buffer.Buffer) (folder string, flags []string, err error) 43 | } 44 | -------------------------------------------------------------------------------- /framework/module/module_specific_data.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | // ModSpecificData is a container that allows modules to attach 10 | // additional context data to framework objects such as SMTP connections 11 | // without conflicting with each other and ensuring each module 12 | // gets its own namespace. 13 | // 14 | // It must not be used to store stateful objects that may need 15 | // a specific cleanup routine as ModSpecificData does not provide 16 | // any lifetime management. 17 | // 18 | // Stored data must be serializable to JSON for state persistence 19 | // e.g. when message is stored in a on-disk queue. 20 | type ModSpecificData struct { 21 | modDataLck sync.RWMutex 22 | modData map[string]interface{} 23 | } 24 | 25 | func (msd *ModSpecificData) modKey(m Module, perInstance bool) string { 26 | if !perInstance { 27 | return m.Name() 28 | } 29 | instName := m.InstanceName() 30 | if instName == "" { 31 | instName = fmt.Sprintf("%x", m) 32 | } 33 | return m.Name() + "/" + instName 34 | } 35 | 36 | func (msd *ModSpecificData) MarshalJSON() ([]byte, error) { 37 | msd.modDataLck.RLock() 38 | defer msd.modDataLck.RUnlock() 39 | return json.Marshal(msd.modData) 40 | } 41 | 42 | func (msd *ModSpecificData) UnmarshalJSON(b []byte) error { 43 | msd.modDataLck.Lock() 44 | defer msd.modDataLck.Unlock() 45 | return json.Unmarshal(b, &msd.modData) 46 | } 47 | 48 | func (msd *ModSpecificData) Set(m Module, perInstance bool, value interface{}) { 49 | key := msd.modKey(m, perInstance) 50 | msd.modDataLck.Lock() 51 | defer msd.modDataLck.Unlock() 52 | if msd.modData == nil { 53 | msd.modData = make(map[string]interface{}) 54 | } 55 | msd.modData[key] = value 56 | } 57 | 58 | func (msd *ModSpecificData) Get(m Module, perInstance bool) interface{} { 59 | key := msd.modKey(m, perInstance) 60 | msd.modDataLck.RLock() 61 | defer msd.modDataLck.RUnlock() 62 | return msd.modData[key] 63 | } 64 | -------------------------------------------------------------------------------- /framework/module/storage.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package module 20 | 21 | import ( 22 | imapbackend "github.com/emersion/go-imap/backend" 23 | ) 24 | 25 | // Storage interface is a slightly modified go-imap's Backend interface 26 | // (authentication is removed). 27 | // 28 | // Modules implementing this interface should be registered with prefix 29 | // "storage." in name. 30 | type Storage interface { 31 | // GetOrCreateIMAPAcct returns User associated with storage account specified by 32 | // the name. 33 | // 34 | // If it doesn't exists - it should be created. 35 | GetOrCreateIMAPAcct(username string) (imapbackend.User, error) 36 | GetIMAPAcct(username string) (imapbackend.User, error) 37 | 38 | // Extensions returns list of IMAP extensions supported by backend. 39 | IMAPExtensions() []string 40 | } 41 | 42 | // ManageableStorage is an extended Storage interface that allows to 43 | // list existing accounts, create and delete them. 44 | type ManageableStorage interface { 45 | Storage 46 | 47 | ListIMAPAccts() ([]string, error) 48 | CreateIMAPAcct(username string) error 49 | DeleteIMAPAcct(username string) error 50 | } 51 | -------------------------------------------------------------------------------- /framework/module/table.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package module 20 | 21 | import "context" 22 | 23 | // Table is the interface implemented by module that implementation string-to-string 24 | // translation. 25 | // 26 | // Modules implementing this interface should be registered with prefix 27 | // "table." in name. 28 | type Table interface { 29 | Lookup(ctx context.Context, s string) (string, bool, error) 30 | } 31 | 32 | // MultiTable is the interface that module can implement in addition to Table 33 | // if it can provide multiple values as a lookup result. 34 | type MultiTable interface { 35 | LookupMulti(ctx context.Context, s string) ([]string, error) 36 | } 37 | 38 | type MutableTable interface { 39 | Table 40 | Keys() ([]string, error) 41 | RemoveKey(k string) error 42 | SetKey(k, v string) error 43 | } 44 | -------------------------------------------------------------------------------- /framework/module/tls_loader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package module 20 | 21 | import ( 22 | "crypto/tls" 23 | ) 24 | 25 | // TLSLoader interface is module interface that can be used to supply TLS 26 | // certificates to TLS-enabled endpoints. 27 | // 28 | // The interface is intentionally kept simple, all configuration and parameters 29 | // necessary are to be provided using conventional module configuration. 30 | // 31 | // If loader returns multiple certificate chains - endpoint will serve them 32 | // based on SNI matching. 33 | // 34 | // Note that loading function will be called for each connections - it is 35 | // highly recommended to cache parsed form. 36 | // 37 | // Modules implementing this interface should be registered with prefix 38 | // "tls.loader." in name. 39 | type TLSLoader interface { 40 | ConfigureTLS(c *tls.Config) error 41 | } 42 | -------------------------------------------------------------------------------- /internal/README.md: -------------------------------------------------------------------------------- 1 | maddy source tree 2 | ------------------ 3 | 4 | Main maddy code base lives here. No packages are intended to be used in 5 | third-party software hence API is not stable. 6 | 7 | Subdirectories are organized as follows: 8 | ``` 9 | / 10 | auxiliary libraries 11 | endpoint/ 12 | modules - protocol listeners (e.g. SMTP server, etc) 13 | target/ 14 | modules - final delivery targets (including outbound delivery, such as 15 | target.smtp, remote) 16 | auth/ 17 | modules - authentication providers 18 | check/ 19 | modules - message checkers (module.Check) 20 | modify/ 21 | modules - message modifiers (module.Modifier) 22 | storage/ 23 | modules - local messages storage implementations (module.Storage) 24 | ``` 25 | -------------------------------------------------------------------------------- /internal/auth/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package auth 20 | 21 | import "strings" 22 | 23 | func CheckDomainAuth(username string, perDomain bool, allowedDomains []string) (loginName string, allowed bool) { 24 | var accountName, domain string 25 | if perDomain { 26 | parts := strings.Split(username, "@") 27 | if len(parts) != 2 { 28 | return "", false 29 | } 30 | domain = parts[1] 31 | accountName = username 32 | } else { 33 | parts := strings.Split(username, "@") 34 | accountName = parts[0] 35 | if len(parts) == 2 { 36 | domain = parts[1] 37 | } 38 | } 39 | 40 | allowed = domain == "" 41 | if allowedDomains != nil && domain != "" { 42 | for _, allowedDomain := range allowedDomains { 43 | if strings.EqualFold(domain, allowedDomain) { 44 | allowed = true 45 | } 46 | } 47 | if !allowed { 48 | return "", false 49 | } 50 | } 51 | 52 | return accountName, allowed 53 | } 54 | -------------------------------------------------------------------------------- /internal/auth/external/helperauth.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package external 20 | 21 | import ( 22 | "fmt" 23 | "io" 24 | "os/exec" 25 | 26 | "github.com/foxcpp/maddy/framework/module" 27 | ) 28 | 29 | func AuthUsingHelper(binaryPath, accountName, password string) error { 30 | cmd := exec.Command(binaryPath) 31 | stdin, err := cmd.StdinPipe() 32 | if err != nil { 33 | return fmt.Errorf("helperauth: stdin init: %w", err) 34 | } 35 | if err := cmd.Start(); err != nil { 36 | return fmt.Errorf("helperauth: process start: %w", err) 37 | } 38 | if _, err := io.WriteString(stdin, accountName+"\n"); err != nil { 39 | return fmt.Errorf("helperauth: stdin write: %w", err) 40 | } 41 | if _, err := io.WriteString(stdin, password+"\n"); err != nil { 42 | return fmt.Errorf("helperauth: stdin write: %w", err) 43 | } 44 | if err := cmd.Wait(); err != nil { 45 | if exitErr, ok := err.(*exec.ExitError); ok { 46 | // Exit code 1 is for authentication failure. 47 | if exitErr.ExitCode() != 1 { 48 | return fmt.Errorf("helperauth: %w: %v", err, string(exitErr.Stderr)) 49 | } 50 | return module.ErrUnknownCredentials 51 | } 52 | return fmt.Errorf("helperauth: process wait: %w", err) 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/auth/pam/pam.go: -------------------------------------------------------------------------------- 1 | //go:build cgo && libpam 2 | // +build cgo,libpam 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package pam 23 | 24 | /* 25 | #cgo LDFLAGS: -lpam 26 | #cgo CFLAGS: -DCGO -Wall -Wextra -Werror -Wno-unused-parameter -Wno-error=unused-parameter -Wpedantic -std=c99 27 | 28 | #include 29 | #include "pam.h" 30 | */ 31 | import "C" 32 | 33 | import ( 34 | "errors" 35 | "fmt" 36 | "unsafe" 37 | ) 38 | 39 | const canCallDirectly = true 40 | 41 | var ErrInvalidCredentials = errors.New("pam: invalid credentials or unknown user") 42 | 43 | func runPAMAuth(username, password string) error { 44 | usernameC := C.CString(username) 45 | passwordC := C.CString(password) 46 | defer C.free(unsafe.Pointer(usernameC)) 47 | defer C.free(unsafe.Pointer(passwordC)) 48 | errObj := C.run_pam_auth(usernameC, passwordC) 49 | if errObj.status == 1 { 50 | return ErrInvalidCredentials 51 | } 52 | if errObj.status == 2 { 53 | return fmt.Errorf("%s: %s", C.GoString(errObj.func_name), C.GoString(errObj.error_msg)) 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/auth/pam/pam.h: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | struct error_obj { 22 | int status; 23 | const char* func_name; 24 | const char* error_msg; 25 | }; 26 | 27 | struct error_obj run_pam_auth(const char *username, char *password); 28 | -------------------------------------------------------------------------------- /internal/auth/pam/pam_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !cgo || !libpam 2 | // +build !cgo !libpam 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package pam 23 | 24 | import ( 25 | "errors" 26 | ) 27 | 28 | const canCallDirectly = false 29 | 30 | var ErrInvalidCredentials = errors.New("pam: invalid credentials or unknown user") 31 | 32 | func runPAMAuth(username, password string) error { 33 | return errors.New("pam: Can't call libpam directly") 34 | } 35 | -------------------------------------------------------------------------------- /internal/auth/sasllogin/sasllogin.go: -------------------------------------------------------------------------------- 1 | package sasllogin 2 | 3 | import "github.com/emersion/go-sasl" 4 | 5 | // Copy-pasted from old emersion/go-sasl version 6 | 7 | // Authenticates users with an username and a password. 8 | type LoginAuthenticator func(username, password string) error 9 | type loginState int 10 | 11 | const ( 12 | loginNotStarted loginState = iota 13 | loginWaitingUsername 14 | loginWaitingPassword 15 | ) 16 | 17 | type loginServer struct { 18 | state loginState 19 | username, password string 20 | authenticate LoginAuthenticator 21 | } 22 | 23 | // A server implementation of the LOGIN authentication mechanism, as described 24 | // in https://tools.ietf.org/html/draft-murchison-sasl-login-00. 25 | // 26 | // LOGIN is obsolete and should only be enabled for legacy clients that cannot 27 | // be updated to use PLAIN. 28 | func NewLoginServer(authenticator LoginAuthenticator) sasl.Server { 29 | return &loginServer{authenticate: authenticator} 30 | } 31 | 32 | func (a *loginServer) Next(response []byte) (challenge []byte, done bool, err error) { 33 | switch a.state { 34 | case loginNotStarted: 35 | // Check for initial response field, as per RFC4422 section 3 36 | if response == nil { 37 | challenge = []byte("Username:") 38 | break 39 | } 40 | a.state++ 41 | fallthrough 42 | case loginWaitingUsername: 43 | a.username = string(response) 44 | challenge = []byte("Password:") 45 | case loginWaitingPassword: 46 | a.password = string(response) 47 | err = a.authenticate(a.username, a.password) 48 | done = true 49 | default: 50 | err = sasl.ErrUnexpectedClientResponse 51 | } 52 | a.state++ 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /internal/authz/lookup.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/foxcpp/maddy/framework/address" 8 | "github.com/foxcpp/maddy/framework/module" 9 | ) 10 | 11 | func AuthorizeEmailUse(ctx context.Context, username string, addrs []string, mapping module.Table) (bool, error) { 12 | var validEmails []string 13 | 14 | if multi, ok := mapping.(module.MultiTable); ok { 15 | var err error 16 | validEmails, err = multi.LookupMulti(ctx, username) 17 | if err != nil { 18 | return false, fmt.Errorf("authz: %w", err) 19 | } 20 | } else { 21 | validEmail, ok, err := mapping.Lookup(ctx, username) 22 | if err != nil { 23 | return false, fmt.Errorf("authz: %w", err) 24 | } 25 | if ok { 26 | validEmails = []string{validEmail} 27 | } 28 | } 29 | 30 | for _, addr := range addrs { 31 | _, domain, err := address.Split(addr) 32 | if err != nil { 33 | return false, fmt.Errorf("authz: %w", err) 34 | } 35 | 36 | for _, ent := range validEmails { 37 | if ent == domain || ent == "*" || ent == addr { 38 | return true, nil 39 | } 40 | } 41 | } 42 | 43 | return false, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/authz/normalization.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/foxcpp/maddy/framework/address" 7 | "golang.org/x/text/secure/precis" 8 | ) 9 | 10 | type NormalizeFunc func(string) (string, error) 11 | 12 | func NormalizeNoop(s string) (string, error) { 13 | return s, nil 14 | } 15 | 16 | // NormalizeAuto applies address.PRECISFold to valid emails and 17 | // plain UsernameCaseMapped profile to other strings. 18 | func NormalizeAuto(s string) (string, error) { 19 | if address.Valid(s) { 20 | return address.PRECISFold(s) 21 | } 22 | return precis.UsernameCaseMapped.CompareKey(s) 23 | } 24 | 25 | // NormalizeFuncs defines configurable normalization functions to be used 26 | // in authentication and authorization routines. 27 | var NormalizeFuncs = map[string]NormalizeFunc{ 28 | "auto": NormalizeAuto, 29 | "precis_casefold_email": address.PRECISFold, 30 | "precis_casefold": precis.UsernameCaseMapped.CompareKey, 31 | "precis_email": address.PRECIS, 32 | "precis": precis.UsernameCasePreserved.CompareKey, 33 | "casefold": func(s string) (string, error) { 34 | return strings.ToLower(s), nil 35 | }, 36 | "noop": NormalizeNoop, 37 | } 38 | -------------------------------------------------------------------------------- /internal/check/milter/milter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package milter 20 | 21 | import ( 22 | "testing" 23 | 24 | "github.com/foxcpp/maddy/framework/config" 25 | ) 26 | 27 | func TestAcceptValidEndpoints(t *testing.T) { 28 | for _, endpoint := range []string{ 29 | "tcp://0.0.0.0:10025", 30 | "tcp://[::]:10025", 31 | "tcp:127.0.0.1:10025", 32 | "unix://path", 33 | "unix:path", 34 | "unix:/path", 35 | "unix:///path", 36 | "unix://also/path", 37 | "unix:///also/path", 38 | } { 39 | c := &Check{milterUrl: endpoint} 40 | 41 | err := c.Init(&config.Map{}) 42 | if err != nil { 43 | t.Errorf("Unexpected failure for %s: %v", endpoint, err) 44 | return 45 | } 46 | } 47 | } 48 | 49 | func TestRejectInvalidEndpoints(t *testing.T) { 50 | for _, endpoint := range []string{ 51 | "tls://0.0.0.0:10025", 52 | "tls:0.0.0.0:10025", 53 | } { 54 | c := &Check{milterUrl: endpoint} 55 | err := c.Init(&config.Map{}) 56 | if err == nil { 57 | t.Errorf("Accepted invalid endpoint: %s", endpoint) 58 | return 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/check/requiretls/requiretls.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package requiretls 20 | 21 | import ( 22 | modconfig "github.com/foxcpp/maddy/framework/config/module" 23 | "github.com/foxcpp/maddy/framework/exterrors" 24 | "github.com/foxcpp/maddy/framework/module" 25 | "github.com/foxcpp/maddy/internal/check" 26 | ) 27 | 28 | func requireTLS(ctx check.StatelessCheckContext) module.CheckResult { 29 | if ctx.MsgMeta.Conn != nil && ctx.MsgMeta.Conn.TLS.HandshakeComplete { 30 | return module.CheckResult{} 31 | } 32 | 33 | return module.CheckResult{ 34 | Reason: &exterrors.SMTPError{ 35 | Code: 550, 36 | EnhancedCode: exterrors.EnhancedCode{5, 7, 1}, 37 | Message: "TLS conversation required", 38 | CheckName: "require_tls", 39 | }, 40 | } 41 | } 42 | 43 | func init() { 44 | check.RegisterStatelessCheck("require_tls", modconfig.FailAction{Reject: true}, requireTLS, nil, nil, nil) 45 | } 46 | -------------------------------------------------------------------------------- /internal/cli/clitools/termios_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package clitools 23 | 24 | import ( 25 | "errors" 26 | "os" 27 | ) 28 | 29 | type Termios struct { 30 | Iflag uint32 31 | Oflag uint32 32 | Cflag uint32 33 | Lflag uint32 34 | Cc [20]byte 35 | Ispeed uint32 36 | Ospeed uint32 37 | } 38 | 39 | func TurnOnRawIO(tty *os.File) (orig Termios, err error) { 40 | return Termios{}, errors.New("not implemented") 41 | } 42 | 43 | func TcSetAttr(fd uintptr, termios *Termios) error { 44 | return errors.New("not implemented") 45 | } 46 | 47 | func TcGetAttr(fd uintptr) (*Termios, error) { 48 | return nil, errors.New("not implemented") 49 | } 50 | -------------------------------------------------------------------------------- /internal/cli/extflag.go: -------------------------------------------------------------------------------- 1 | package maddycli 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | // extFlag implements cli.Flag via standard flag.Flag. 10 | type extFlag struct { 11 | f *flag.Flag 12 | } 13 | 14 | func (e *extFlag) Apply(fs *flag.FlagSet) error { 15 | fs.Var(e.f.Value, e.f.Name, e.f.Usage) 16 | return nil 17 | } 18 | 19 | func (e *extFlag) Names() []string { 20 | return []string{e.f.Name} 21 | } 22 | 23 | func (e *extFlag) IsSet() bool { 24 | return false 25 | } 26 | 27 | func (e *extFlag) String() string { 28 | return cli.FlagStringer(e) 29 | } 30 | 31 | func (e *extFlag) IsVisible() bool { 32 | return true 33 | } 34 | 35 | func (e *extFlag) TakesValue() bool { 36 | return false 37 | } 38 | 39 | func (e *extFlag) GetUsage() string { 40 | return e.f.Usage 41 | } 42 | 43 | func (e *extFlag) GetValue() string { 44 | return e.f.Value.String() 45 | } 46 | 47 | func (e *extFlag) GetDefaultText() string { 48 | return e.f.DefValue 49 | } 50 | 51 | func (e *extFlag) GetEnvVars() []string { 52 | return nil 53 | } 54 | 55 | func mapStdlibFlags(app *cli.App) { 56 | // Modified AllowExtFlags from cli lib with -test.* exception removed. 57 | flag.VisitAll(func(f *flag.Flag) { 58 | app.Flags = append(app.Flags, &extFlag{f}) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /internal/dmarc/dmarc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package dmarc 20 | 21 | import ( 22 | "context" 23 | 24 | "github.com/emersion/go-msgauth/dmarc" 25 | ) 26 | 27 | type ( 28 | Resolver interface { 29 | LookupTXT(context.Context, string) ([]string, error) 30 | } 31 | 32 | Record = dmarc.Record 33 | Policy = dmarc.Policy 34 | AlignmentMode = dmarc.AlignmentMode 35 | FailureOptions = dmarc.FailureOptions 36 | ) 37 | 38 | const ( 39 | PolicyNone = dmarc.PolicyNone 40 | PolicyReject = dmarc.PolicyReject 41 | PolicyQuarantine = dmarc.PolicyQuarantine 42 | ) 43 | -------------------------------------------------------------------------------- /internal/endpoint/dovecot_sasld/mech_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package dovecotsasld 20 | 21 | import ( 22 | "github.com/emersion/go-sasl" 23 | dovecotsasl "github.com/foxcpp/go-dovecot-sasl" 24 | ) 25 | 26 | var mechInfo = map[string]dovecotsasl.Mechanism{ 27 | sasl.Plain: { 28 | Plaintext: true, 29 | }, 30 | sasl.Login: { 31 | Plaintext: true, 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /internal/libdns/acmedns.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_acmedns || libdns_all 2 | // +build libdns_acmedns libdns_all 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/acmedns" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.acmedns", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := acmedns.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("username", false, true, "", &p.Username) 20 | c.String("password", false, true, "", &p.Password) 21 | c.String("subdomain", false, true, "", &p.Subdomain) 22 | c.String("server_url", false, true, "", &p.ServerURL) 23 | }, 24 | instName: instName, 25 | modName: modName, 26 | }, nil 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /internal/libdns/alidns.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_alidns || libdns_all 2 | // +build libdns_alidns libdns_all 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/alidns" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.alidns", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := alidns.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("key_id", false, false, "", &p.AccKeyID) 20 | c.String("key_secret", false, false, "", &p.AccKeySecret) 21 | }, 22 | instName: instName, 23 | modName: modName, 24 | }, nil 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /internal/libdns/cloudflare.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_cloudflare || !libdns_separate 2 | // +build libdns_cloudflare !libdns_separate 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/cloudflare" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.cloudflare", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := cloudflare.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("api_token", false, false, "", &p.APIToken) 20 | }, 21 | instName: instName, 22 | modName: modName, 23 | }, nil 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /internal/libdns/digitalocean.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_digitalocean || !libdns_separate 2 | // +build libdns_digitalocean !libdns_separate 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/digitalocean" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.digitalocean", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := digitalocean.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("api_token", false, false, "", &p.APIToken) 20 | }, 21 | instName: instName, 22 | modName: modName, 23 | }, nil 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /internal/libdns/gandi.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_gandi || !libdns_separate 2 | // +build libdns_gandi !libdns_separate 3 | 4 | package libdns 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/foxcpp/maddy/framework/config" 10 | "github.com/foxcpp/maddy/framework/log" 11 | "github.com/foxcpp/maddy/framework/module" 12 | "github.com/libdns/gandi" 13 | ) 14 | 15 | func init() { 16 | module.Register("libdns.gandi", func(modName, instName string, _, _ []string) (module.Module, error) { 17 | p := gandi.Provider{} 18 | return &ProviderModule{ 19 | RecordDeleter: &p, 20 | RecordAppender: &p, 21 | setConfig: func(c *config.Map) { 22 | c.String("api_token", false, false, "", &p.APIToken) 23 | c.String("personal_token", false, false, "", &p.BearerToken) 24 | }, 25 | afterConfig: func() error { 26 | if p.APIToken != "" { 27 | log.Println("libdns.gandi: api_token is deprecated, use personal_token instead (https://api.gandi.net/docs/authentication/)") 28 | } 29 | if p.APIToken == "" && p.BearerToken == "" { 30 | return fmt.Errorf("libdns.gandi: either api_token or personal_token should be specified") 31 | } 32 | return nil 33 | }, 34 | instName: instName, 35 | modName: modName, 36 | }, nil 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /internal/libdns/gcore.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_gcore || !libdns_separate 2 | // +build libdns_gcore !libdns_separate 3 | 4 | package libdns 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/foxcpp/maddy/framework/config" 10 | "github.com/foxcpp/maddy/framework/module" 11 | "github.com/libdns/gcore" 12 | ) 13 | 14 | func init() { 15 | module.Register("libdns.gcore", func(modName, instName string, _, _ []string) (module.Module, error) { 16 | p := gcore.Provider{} 17 | return &ProviderModule{ 18 | RecordDeleter: &p, 19 | RecordAppender: &p, 20 | setConfig: func(c *config.Map) { 21 | c.String("api_key", false, false, "", &p.APIKey) 22 | }, 23 | afterConfig: func() error { 24 | if p.APIKey == "" { 25 | return fmt.Errorf("libdns.gcore: api_key should be specified") 26 | } 27 | return nil 28 | }, 29 | 30 | instName: instName, 31 | modName: modName, 32 | }, nil 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /internal/libdns/googleclouddns.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_googleclouddns || libdns_all 2 | // +build libdns_googleclouddns libdns_all 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/googleclouddns" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.googleclouddns", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := googleclouddns.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("project", false, true, "", &p.Project) 20 | c.String("service_account_json", false, false, "", &p.ServiceAccountJSON) 21 | }, 22 | instName: instName, 23 | modName: modName, 24 | }, nil 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /internal/libdns/hetzner.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_hetzner || !libdns_separate 2 | // +build libdns_hetzner !libdns_separate 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/hetzner" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.hetzner", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := hetzner.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("api_token", false, false, "", &p.AuthAPIToken) 20 | }, 21 | instName: instName, 22 | modName: modName, 23 | }, nil 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /internal/libdns/leaseweb.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_leaseweb || libdns_all 2 | // +build libdns_leaseweb libdns_all 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/leaseweb" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.leaseweb", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := leaseweb.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("api_key", false, false, "", &p.APIKey) 20 | }, 21 | instName: instName, 22 | modName: modName, 23 | }, nil 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /internal/libdns/metaname.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_metaname || libdns_all 2 | // +build libdns_metaname libdns_all 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/metaname" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.metaname", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := metaname.Provider{ 15 | Endpoint: "https://metaname.net/api/1.1", 16 | } 17 | return &ProviderModule{ 18 | RecordDeleter: &p, 19 | RecordAppender: &p, 20 | setConfig: func(c *config.Map) { 21 | c.String("api_key", false, false, "", &p.APIKey) 22 | c.String("account_ref", false, false, "", &p.AccountReference) 23 | }, 24 | instName: instName, 25 | modName: modName, 26 | }, nil 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /internal/libdns/namecheap.go: -------------------------------------------------------------------------------- 1 | //go:build go1.16 2 | // +build go1.16 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/namecheap" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.namecheap", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := namecheap.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("api_key", false, true, "", &p.APIKey) 20 | c.String("api_username", false, true, "", &p.User) 21 | c.String("endpoint", false, false, "", &p.APIEndpoint) 22 | c.String("client_ip", false, false, "", &p.ClientIP) 23 | }, 24 | instName: instName, 25 | modName: modName, 26 | }, nil 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /internal/libdns/namedotcom.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_namedotdom || libdns_all 2 | // +build libdns_namedotdom libdns_all 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/namedotcom" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.namedotcom", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := namedotcom.Provider{ 15 | Server: "https://api.name.com", 16 | } 17 | return &ProviderModule{ 18 | RecordDeleter: &p, 19 | RecordAppender: &p, 20 | setConfig: func(c *config.Map) { 21 | c.String("user", false, false, "", &p.User) 22 | c.String("token", false, false, "", &p.Token) 23 | }, 24 | instName: instName, 25 | modName: modName, 26 | }, nil 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /internal/libdns/provider_module.go: -------------------------------------------------------------------------------- 1 | package libdns 2 | 3 | import ( 4 | "github.com/foxcpp/maddy/framework/config" 5 | "github.com/libdns/libdns" 6 | ) 7 | 8 | type ProviderModule struct { 9 | libdns.RecordDeleter 10 | libdns.RecordAppender 11 | setConfig func(c *config.Map) 12 | afterConfig func() error 13 | 14 | instName string 15 | modName string 16 | } 17 | 18 | func (p *ProviderModule) Init(cfg *config.Map) error { 19 | p.setConfig(cfg) 20 | _, err := cfg.Process() 21 | if p.afterConfig != nil { 22 | if err := p.afterConfig(); err != nil { 23 | return err 24 | } 25 | } 26 | return err 27 | } 28 | 29 | func (p *ProviderModule) Name() string { 30 | return p.modName 31 | } 32 | 33 | func (p *ProviderModule) InstanceName() string { 34 | return p.instName 35 | } 36 | -------------------------------------------------------------------------------- /internal/libdns/rfc2136.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_rfc2136 || libdns_all 2 | // +build libdns_rfc2136 libdns_all 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/rfc2136" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.rfc2136", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := rfc2136.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("key_name", false, true, "", &p.KeyName) 20 | c.String("key", false, true, "", &p.Key) 21 | c.String("key_alg", false, true, "", &p.KeyAlg) 22 | c.String("server", false, true, "", &p.Server) 23 | }, 24 | instName: instName, 25 | modName: modName, 26 | }, nil 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /internal/libdns/route53.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_route53 || libdns_all 2 | // +build libdns_route53 libdns_all 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/route53" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.route53", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := route53.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("secret_access_key", false, false, "", &p.SecretAccessKey) 20 | c.String("access_key_id", false, false, "", &p.AccessKeyId) 21 | }, 22 | instName: instName, 23 | modName: modName, 24 | }, nil 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /internal/libdns/vultr.go: -------------------------------------------------------------------------------- 1 | //go:build libdns_vultr || !libdns_separate 2 | // +build libdns_vultr !libdns_separate 3 | 4 | package libdns 5 | 6 | import ( 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/libdns/vultr" 10 | ) 11 | 12 | func init() { 13 | module.Register("libdns.vultr", func(modName, instName string, _, _ []string) (module.Module, error) { 14 | p := vultr.Provider{} 15 | return &ProviderModule{ 16 | RecordDeleter: &p, 17 | RecordAppender: &p, 18 | setConfig: func(c *config.Map) { 19 | c.String("api_token", false, false, "", &p.APIToken) 20 | }, 21 | instName: instName, 22 | modName: modName, 23 | }, nil 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /internal/limits/limiters/concurrency.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package limiters 20 | 21 | import "context" 22 | 23 | // Semaphore is a convenience wrapper for a channel that implements 24 | // semaphore-kind synchronization. 25 | // 26 | // If the argument given to the NewSemaphore is negative or zero, 27 | // all methods are no-op. 28 | type Semaphore struct { 29 | c chan struct{} 30 | } 31 | 32 | func NewSemaphore(max int) Semaphore { 33 | return Semaphore{c: make(chan struct{}, max)} 34 | } 35 | 36 | func (s Semaphore) Take() bool { 37 | if cap(s.c) <= 0 { 38 | return true 39 | } 40 | s.c <- struct{}{} 41 | return true 42 | } 43 | 44 | func (s Semaphore) TakeContext(ctx context.Context) error { 45 | if cap(s.c) <= 0 { 46 | return nil 47 | } 48 | select { 49 | case s.c <- struct{}{}: 50 | return nil 51 | case <-ctx.Done(): 52 | return ctx.Err() 53 | } 54 | } 55 | 56 | func (s Semaphore) Release() { 57 | if cap(s.c) <= 0 { 58 | return 59 | } 60 | select { 61 | case <-s.c: 62 | default: 63 | panic("limiters: mismatched Release call") 64 | } 65 | } 66 | 67 | func (s Semaphore) Close() { 68 | } 69 | -------------------------------------------------------------------------------- /internal/limits/limiters/limiters.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | // Package limiters provides a set of wrappers intended to restrict the amount 20 | // of resources consumed by the server. 21 | package limiters 22 | 23 | import "context" 24 | 25 | // The L interface represents a blocking limiter that has some upper bound of 26 | // resource use and blocks when it is exceeded until enough resources are 27 | // freed. 28 | type L interface { 29 | Take() bool 30 | TakeContext(context.Context) error 31 | Release() 32 | 33 | // Close frees any resources used internally by Limiter for book-keeping. 34 | Close() 35 | } 36 | -------------------------------------------------------------------------------- /internal/limits/limiters/multilimit.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package limiters 20 | 21 | import "context" 22 | 23 | // MultiLimit wraps multiple L implementations into a single one, locking them 24 | // in the specified order. 25 | // 26 | // It does not implement any deadlock detection or avoidance algorithms. 27 | type MultiLimit struct { 28 | Wrapped []L 29 | } 30 | 31 | func (ml *MultiLimit) Take() bool { 32 | for i := 0; i < len(ml.Wrapped); i++ { 33 | if !ml.Wrapped[i].Take() { 34 | // Acquire failed, undo acquire for all other resources we already 35 | // got. 36 | for _, l := range ml.Wrapped[:i] { 37 | l.Release() 38 | } 39 | return false 40 | } 41 | } 42 | return true 43 | } 44 | 45 | func (ml *MultiLimit) TakeContext(ctx context.Context) error { 46 | for i := 0; i < len(ml.Wrapped); i++ { 47 | if err := ml.Wrapped[i].TakeContext(ctx); err != nil { 48 | // Acquire failed, undo acquire for all other resources we already 49 | // got. 50 | for _, l := range ml.Wrapped[:i] { 51 | l.Release() 52 | } 53 | return err 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | func (ml *MultiLimit) Release() { 60 | for _, l := range ml.Wrapped { 61 | l.Release() 62 | } 63 | } 64 | 65 | func (ml *MultiLimit) Close() { 66 | for _, l := range ml.Wrapped { 67 | l.Close() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/msgpipeline/metrics.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package msgpipeline 20 | 21 | import "github.com/prometheus/client_golang/prometheus" 22 | 23 | var ( 24 | checkReject = prometheus.NewCounterVec( 25 | prometheus.CounterOpts{ 26 | Namespace: "maddy", 27 | Subsystem: "check", 28 | Name: "reject", 29 | Help: "Number of times a check returned 'reject' result (may be more than processed messages if check does so on per-recipient basis)", 30 | }, 31 | []string{"check"}, 32 | ) 33 | checkQuarantined = prometheus.NewCounterVec( 34 | prometheus.CounterOpts{ 35 | Namespace: "maddy", 36 | Subsystem: "check", 37 | Name: "quarantined", 38 | Help: "Number of times a check returned 'quarantine' result (may be more than processed messages if check does so on per-recipient basis)", 39 | }, 40 | []string{"check"}, 41 | ) 42 | ) 43 | 44 | func init() { 45 | prometheus.MustRegister(checkReject) 46 | prometheus.MustRegister(checkQuarantined) 47 | } 48 | -------------------------------------------------------------------------------- /internal/msgpipeline/module.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package msgpipeline 20 | 21 | import ( 22 | "github.com/foxcpp/maddy/framework/config" 23 | "github.com/foxcpp/maddy/framework/log" 24 | "github.com/foxcpp/maddy/framework/module" 25 | ) 26 | 27 | type Module struct { 28 | instName string 29 | log log.Logger 30 | *MsgPipeline 31 | } 32 | 33 | func NewModule(modName, instName string, aliases, inlineArgs []string) (module.Module, error) { 34 | return &Module{ 35 | log: log.Logger{Name: "msgpipeline"}, 36 | instName: instName, 37 | }, nil 38 | } 39 | 40 | func (m *Module) Init(cfg *config.Map) error { 41 | var hostname string 42 | cfg.String("hostname", true, true, "", &hostname) 43 | cfg.Bool("debug", true, false, &m.log.Debug) 44 | cfg.AllowUnknown() 45 | other, err := cfg.Process() 46 | if err != nil { 47 | return err 48 | } 49 | 50 | p, err := New(cfg.Globals, other) 51 | if err != nil { 52 | return err 53 | } 54 | m.MsgPipeline = p 55 | m.MsgPipeline.Log = m.log 56 | 57 | return nil 58 | } 59 | 60 | func (m *Module) Name() string { 61 | return "msgpipeline" 62 | } 63 | 64 | func (m *Module) InstanceName() string { 65 | return m.instName 66 | } 67 | 68 | func init() { 69 | module.Register("msgpipeline", NewModule) 70 | } 71 | -------------------------------------------------------------------------------- /internal/msgpipeline/objname.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package msgpipeline 20 | 21 | import ( 22 | "fmt" 23 | 24 | "github.com/foxcpp/maddy/framework/module" 25 | ) 26 | 27 | // objectName returns a new that is usable to identify the used external 28 | // component (module or some stub) in debug logs. 29 | func objectName(x interface{}) string { 30 | mod, ok := x.(module.Module) 31 | if ok { 32 | return mod.Name() + ":" + mod.InstanceName() 33 | } 34 | 35 | _, pipeline := x.(*MsgPipeline) 36 | if pipeline { 37 | return "reroute" 38 | } 39 | 40 | str, ok := x.(fmt.Stringer) 41 | if ok { 42 | return str.String() 43 | } 44 | 45 | return fmt.Sprintf("%T", x) 46 | } 47 | -------------------------------------------------------------------------------- /internal/smtpconn/smtpconn_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package smtpconn 20 | 21 | import ( 22 | "flag" 23 | "math/rand" 24 | "os" 25 | "strconv" 26 | "testing" 27 | ) 28 | 29 | var testPort string 30 | 31 | func TestMain(m *testing.M) { 32 | remoteSmtpPort := flag.String("test.smtpport", "random", "(maddy) SMTP port to use for connections in tests") 33 | flag.Parse() 34 | 35 | if *remoteSmtpPort == "random" { 36 | *remoteSmtpPort = strconv.Itoa(rand.Intn(65536-10000) + 10000) 37 | } 38 | 39 | testPort = *remoteSmtpPort 40 | os.Exit(m.Run()) 41 | } 42 | -------------------------------------------------------------------------------- /internal/storage/blob/fs/fs_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/foxcpp/maddy/framework/module" 8 | "github.com/foxcpp/maddy/internal/storage/blob" 9 | "github.com/foxcpp/maddy/internal/testutils" 10 | ) 11 | 12 | func TestFS(t *testing.T) { 13 | blob.TestStore(t, func() module.BlobStore { 14 | dir := testutils.Dir(t) 15 | return &FSStore{instName: "test", root: dir} 16 | }, func(store module.BlobStore) { 17 | os.RemoveAll(store.(*FSStore).root) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /internal/storage/blob/s3/s3_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/foxcpp/maddy/framework/config" 8 | "github.com/foxcpp/maddy/framework/module" 9 | "github.com/foxcpp/maddy/internal/storage/blob" 10 | "github.com/johannesboyne/gofakes3" 11 | "github.com/johannesboyne/gofakes3/backend/s3mem" 12 | ) 13 | 14 | func TestFS(t *testing.T) { 15 | var ( 16 | backend gofakes3.Backend 17 | faker *gofakes3.GoFakeS3 18 | ts *httptest.Server 19 | ) 20 | 21 | blob.TestStore(t, func() module.BlobStore { 22 | backend = s3mem.New() 23 | faker = gofakes3.New(backend) 24 | ts = httptest.NewServer(faker.Server()) 25 | 26 | if err := backend.CreateBucket("maddy-test"); err != nil { 27 | panic(err) 28 | } 29 | 30 | st := &Store{instName: "test"} 31 | err := st.Init(config.NewMap(map[string]interface{}{}, config.Node{ 32 | Children: []config.Node{ 33 | { 34 | Name: "endpoint", 35 | Args: []string{ts.Listener.Addr().String()}, 36 | }, 37 | { 38 | Name: "secure", 39 | Args: []string{"false"}, 40 | }, 41 | { 42 | Name: "access_key", 43 | Args: []string{"access-key"}, 44 | }, 45 | { 46 | Name: "secret_key", 47 | Args: []string{"secret-key"}, 48 | }, 49 | { 50 | Name: "bucket", 51 | Args: []string{"maddy-test"}, 52 | }, 53 | }, 54 | })) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | return st 60 | }, func(store module.BlobStore) { 61 | ts.Close() 62 | 63 | backend = s3mem.New() 64 | faker = gofakes3.New(backend) 65 | ts = httptest.NewServer(faker.Server()) 66 | }) 67 | 68 | if ts != nil { 69 | ts.Close() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/storage/blob/test_blob.go: -------------------------------------------------------------------------------- 1 | //go:build cgo && !no_sqlite3 2 | // +build cgo,!no_sqlite3 3 | 4 | package blob 5 | 6 | import ( 7 | "math/rand" 8 | "testing" 9 | 10 | backendtests "github.com/foxcpp/go-imap-backend-tests" 11 | imapsql "github.com/foxcpp/go-imap-sql" 12 | "github.com/foxcpp/maddy/framework/module" 13 | imapsql2 "github.com/foxcpp/maddy/internal/storage/imapsql" 14 | "github.com/foxcpp/maddy/internal/testutils" 15 | ) 16 | 17 | type testBack struct { 18 | backendtests.Backend 19 | ExtStore module.BlobStore 20 | } 21 | 22 | func TestStore(t *testing.T, newStore func() module.BlobStore, cleanStore func(module.BlobStore)) { 23 | // We use go-imap-sql backend and run a subset of 24 | // go-imap-backend-tests related to loading and saving messages. 25 | // 26 | // In the future we should probably switch to using a memory 27 | // backend for this. 28 | 29 | backendtests.Whitelist = []string{ 30 | t.Name() + "/Mailbox_CreateMessage", 31 | t.Name() + "/Mailbox_ListMessages_Body", 32 | t.Name() + "/Mailbox_CopyMessages", 33 | t.Name() + "/Mailbox_Expunge", 34 | t.Name() + "/Mailbox_MoveMessages", 35 | } 36 | 37 | initBackend := func() backendtests.Backend { 38 | randSrc := rand.NewSource(0) 39 | prng := rand.New(randSrc) 40 | store := newStore() 41 | 42 | b, err := imapsql.New("sqlite3", ":memory:", 43 | imapsql2.ExtBlobStore{Base: store}, imapsql.Opts{ 44 | PRNG: prng, 45 | Log: testutils.Logger(t, "imapsql"), 46 | }, 47 | ) 48 | if err != nil { 49 | panic(err) 50 | } 51 | return testBack{Backend: b, ExtStore: store} 52 | } 53 | cleanBackend := func(bi backendtests.Backend) { 54 | b := bi.(testBack) 55 | if err := b.Backend.(*imapsql.Backend).Close(); err != nil { 56 | panic(err) 57 | } 58 | cleanStore(b.ExtStore) 59 | } 60 | 61 | backendtests.RunTests(t, initBackend, cleanBackend) 62 | } 63 | -------------------------------------------------------------------------------- /internal/storage/blob/test_blob_nosqlite.go: -------------------------------------------------------------------------------- 1 | //go:build !cgo || no_sqlite3 2 | // +build !cgo no_sqlite3 3 | 4 | package blob 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/foxcpp/maddy/framework/module" 10 | ) 11 | 12 | func TestStore(t *testing.T, newStore func() module.BlobStore, cleanStore func(module.BlobStore)) { 13 | t.Skip("storage.blob tests require CGo and sqlite3") 14 | } 15 | -------------------------------------------------------------------------------- /internal/storage/imapsql/external_blob_store.go: -------------------------------------------------------------------------------- 1 | package imapsql 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | imapsql "github.com/foxcpp/go-imap-sql" 8 | "github.com/foxcpp/maddy/framework/module" 9 | ) 10 | 11 | type ExtBlob struct { 12 | io.ReadCloser 13 | } 14 | 15 | func (e ExtBlob) Sync() error { 16 | panic("not implemented") 17 | } 18 | 19 | func (e ExtBlob) Write(p []byte) (n int, err error) { 20 | panic("not implemented") 21 | } 22 | 23 | type WriteExtBlob struct { 24 | module.Blob 25 | } 26 | 27 | func (w WriteExtBlob) Read(p []byte) (n int, err error) { 28 | panic("not implemented") 29 | } 30 | 31 | type ExtBlobStore struct { 32 | Base module.BlobStore 33 | } 34 | 35 | func (e ExtBlobStore) Create(key string, objSize int64) (imapsql.ExtStoreObj, error) { 36 | blob, err := e.Base.Create(context.TODO(), key, objSize) 37 | if err != nil { 38 | return nil, imapsql.ExternalError{ 39 | NonExistent: err == module.ErrNoSuchBlob, 40 | Key: key, 41 | Err: err, 42 | } 43 | } 44 | return WriteExtBlob{Blob: blob}, nil 45 | } 46 | 47 | func (e ExtBlobStore) Open(key string) (imapsql.ExtStoreObj, error) { 48 | blob, err := e.Base.Open(context.TODO(), key) 49 | if err != nil { 50 | return nil, imapsql.ExternalError{ 51 | NonExistent: err == module.ErrNoSuchBlob, 52 | Key: key, 53 | Err: err, 54 | } 55 | } 56 | return ExtBlob{ReadCloser: blob}, nil 57 | } 58 | 59 | func (e ExtBlobStore) Delete(keys []string) error { 60 | err := e.Base.Delete(context.TODO(), keys) 61 | if err != nil { 62 | return imapsql.ExternalError{ 63 | Key: "", 64 | Err: err, 65 | } 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/storage/imapsql/maddyctl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package imapsql 20 | 21 | import ( 22 | "github.com/emersion/go-imap/backend" 23 | ) 24 | 25 | // These methods wrap corresponding go-imap-sql methods, but also apply 26 | // maddy-specific credentials rules. 27 | 28 | func (store *Storage) ListIMAPAccts() ([]string, error) { 29 | return store.Back.ListUsers() 30 | } 31 | 32 | func (store *Storage) CreateIMAPAcct(accountName string) error { 33 | return store.Back.CreateUser(accountName) 34 | } 35 | 36 | func (store *Storage) DeleteIMAPAcct(accountName string) error { 37 | return store.Back.DeleteUser(accountName) 38 | } 39 | 40 | func (store *Storage) GetIMAPAcct(accountName string) (backend.User, error) { 41 | return store.Back.GetUser(accountName) 42 | } 43 | -------------------------------------------------------------------------------- /internal/storage/imapsql/modernc_sqlite3.go: -------------------------------------------------------------------------------- 1 | //go:build !nosqlite3 && !cgo 2 | // +build !nosqlite3,!cgo 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package imapsql 23 | 24 | import _ "modernc.org/sqlite" 25 | 26 | const sqliteImpl = "modernc" 27 | -------------------------------------------------------------------------------- /internal/storage/imapsql/no_sqlite3.go: -------------------------------------------------------------------------------- 1 | //go:build nosqlite3 2 | // +build nosqlite3 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package imapsql 23 | 24 | const sqliteImpl = "missing" 25 | -------------------------------------------------------------------------------- /internal/storage/imapsql/sqlite3.go: -------------------------------------------------------------------------------- 1 | //go:build !nosqlite3 && cgo 2 | // +build !nosqlite3,cgo 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package imapsql 23 | 24 | import _ "github.com/mattn/go-sqlite3" 25 | 26 | const sqliteImpl = "cgo" 27 | -------------------------------------------------------------------------------- /internal/table/identity.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package table 20 | 21 | import ( 22 | "context" 23 | 24 | "github.com/foxcpp/maddy/framework/config" 25 | "github.com/foxcpp/maddy/framework/module" 26 | ) 27 | 28 | type Identity struct { 29 | modName string 30 | instName string 31 | } 32 | 33 | func NewIdentity(modName, instName string, _, _ []string) (module.Module, error) { 34 | return &Identity{ 35 | modName: modName, 36 | instName: instName, 37 | }, nil 38 | } 39 | 40 | func (s *Identity) Init(cfg *config.Map) error { 41 | return nil 42 | } 43 | 44 | func (s *Identity) Name() string { 45 | return s.modName 46 | } 47 | 48 | func (s *Identity) InstanceName() string { 49 | return s.modName 50 | } 51 | 52 | func (s *Identity) Lookup(_ context.Context, key string) (string, bool, error) { 53 | return key, true, nil 54 | } 55 | 56 | func init() { 57 | module.Register("table.identity", NewIdentity) 58 | } 59 | -------------------------------------------------------------------------------- /internal/table/sqlite3.go: -------------------------------------------------------------------------------- 1 | //go:build !nosqlite3 && cgo 2 | // +build !nosqlite3,cgo 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package table 23 | 24 | import _ "github.com/mattn/go-sqlite3" 25 | -------------------------------------------------------------------------------- /internal/target/delivery.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package target 20 | 21 | import ( 22 | "github.com/foxcpp/maddy/framework/log" 23 | "github.com/foxcpp/maddy/framework/module" 24 | ) 25 | 26 | func DeliveryLogger(l log.Logger, msgMeta *module.MsgMetadata) log.Logger { 27 | fields := make(map[string]interface{}, len(l.Fields)+1) 28 | for k, v := range l.Fields { 29 | fields[k] = v 30 | } 31 | fields["msg_id"] = msgMeta.ID 32 | l.Fields = fields 33 | return l 34 | } 35 | -------------------------------------------------------------------------------- /internal/target/queue/metrics.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package queue 20 | 21 | import "github.com/prometheus/client_golang/prometheus" 22 | 23 | var queuedMsgs = prometheus.NewGaugeVec( 24 | prometheus.GaugeOpts{ 25 | Namespace: "maddy", 26 | Subsystem: "queue", 27 | Name: "length", 28 | Help: "Amount of queued messages", 29 | }, 30 | []string{"module", "location"}, 31 | ) 32 | 33 | func init() { 34 | prometheus.MustRegister(queuedMsgs) 35 | } 36 | -------------------------------------------------------------------------------- /internal/target/remote/debugflags.go: -------------------------------------------------------------------------------- 1 | //go:build debugflags 2 | // +build debugflags 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package remote 23 | 24 | import ( 25 | maddycli "github.com/foxcpp/maddy/internal/cli" 26 | "github.com/urfave/cli/v2" 27 | ) 28 | 29 | func init() { 30 | maddycli.AddGlobalFlag(&cli.StringFlag{ 31 | Name: "debug.smtpport", 32 | Usage: "SMTP port to use for connections in tests", 33 | Destination: &smtpPort, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /internal/target/remote/metrics.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package remote 20 | 21 | import "github.com/prometheus/client_golang/prometheus" 22 | 23 | var mxLevelCnt = prometheus.NewCounterVec( 24 | prometheus.CounterOpts{ 25 | Namespace: "maddy", 26 | Subsystem: "remote", 27 | Name: "conns_mx_level", 28 | Help: "Outbound connections established with specific MX security level", 29 | }, 30 | []string{"module", "level"}, 31 | ) 32 | 33 | var tlsLevelCnt = prometheus.NewCounterVec( 34 | prometheus.CounterOpts{ 35 | Namespace: "maddy", 36 | Subsystem: "remote", 37 | Name: "conns_tls_level", 38 | Help: "Outbound connections established with specific TLS security level", 39 | }, 40 | []string{"module", "level"}, 41 | ) 42 | 43 | func init() { 44 | prometheus.MustRegister(mxLevelCnt) 45 | prometheus.MustRegister(tlsLevelCnt) 46 | } 47 | -------------------------------------------------------------------------------- /internal/target/smtp/smtputf8_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package smtp_downstream 20 | 21 | import ( 22 | "testing" 23 | 24 | "github.com/foxcpp/maddy/framework/config" 25 | "github.com/foxcpp/maddy/internal/testutils" 26 | ) 27 | 28 | func TestDownstreamDelivery_EHLO_ALabel(t *testing.T) { 29 | be, srv := testutils.SMTPServer(t, "127.0.0.1:"+testPort) 30 | defer srv.Close() 31 | defer testutils.CheckSMTPConnLeak(t, srv) 32 | 33 | mod, err := NewDownstream("", "", nil, []string{"tcp://127.0.0.1:" + testPort}) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if err := mod.Init(config.NewMap(nil, config.Node{ 38 | Children: []config.Node{ 39 | { 40 | Name: "hostname", 41 | Args: []string{"тест.invalid"}, 42 | }, 43 | { 44 | Name: "starttls", 45 | Args: []string{"no"}, 46 | }, 47 | }, 48 | })); err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | tgt := mod.(*Downstream) 53 | tgt.log = testutils.Logger(t, "remote") 54 | 55 | testutils.DoTestDelivery(t, tgt, "test@example.com", []string{"test@example.invalid"}) 56 | 57 | be.CheckMsg(t, 0, "test@example.com", []string{"test@example.invalid"}) 58 | if be.Messages[0].Conn.Hostname() != "xn--e1aybc.invalid" { 59 | t.Error("target/remote should use use Punycode in EHLO") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/testutils/filesystem.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package testutils 20 | 21 | import ( 22 | "os" 23 | "testing" 24 | ) 25 | 26 | // Dir is a wrapper for os.MkdirTemp that 27 | // fails the test on errors. 28 | func Dir(t *testing.T) string { 29 | dir, err := os.MkdirTemp("", "maddy-tests-") 30 | if err != nil { 31 | t.Fatalf("can't create test dir: %v", err) 32 | } 33 | return dir 34 | } 35 | -------------------------------------------------------------------------------- /internal/testutils/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package testutils 20 | 21 | import ( 22 | "flag" 23 | "os" 24 | "strings" 25 | "testing" 26 | "time" 27 | 28 | "github.com/foxcpp/maddy/framework/log" 29 | ) 30 | 31 | var ( 32 | debugLog = flag.Bool("test.debuglog", false, "(maddy) Turn on debug log messages") 33 | directLog = flag.Bool("test.directlog", false, "(maddy) Log to stderr instead of test log") 34 | ) 35 | 36 | func Logger(t *testing.T, name string) log.Logger { 37 | if *directLog { 38 | return log.Logger{ 39 | Out: log.WriterOutput(os.Stderr, true), 40 | Name: name, 41 | Debug: *debugLog, 42 | } 43 | } 44 | 45 | return log.Logger{ 46 | Out: log.FuncOutput(func(_ time.Time, debug bool, str string) { 47 | t.Helper() 48 | str = strings.TrimSuffix(str, "\n") 49 | if debug { 50 | str = "[debug] " + str 51 | } 52 | t.Log(str) 53 | }, func() error { 54 | return nil 55 | }), 56 | Name: name, 57 | Debug: *debugLog, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/testutils/multitable.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package testutils 20 | 21 | import "context" 22 | 23 | type MultiTable struct { 24 | M map[string][]string 25 | Err error 26 | } 27 | 28 | func (m MultiTable) LookupMulti(_ context.Context, a string) ([]string, error) { 29 | b, ok := m.M[a] 30 | if ok { 31 | return b, m.Err 32 | } else { 33 | return []string{}, m.Err 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/testutils/table.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package testutils 20 | 21 | import "context" 22 | 23 | type Table struct { 24 | M map[string]string 25 | Err error 26 | } 27 | 28 | func (m Table) Lookup(_ context.Context, a string) (string, bool, error) { 29 | b, ok := m.M[a] 30 | return b, ok, m.Err 31 | } 32 | -------------------------------------------------------------------------------- /internal/updatepipe/backend.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package updatepipe 20 | 21 | type BackendMode int 22 | 23 | const ( 24 | // ModeReplicate configures backend to both send and receive updates over 25 | // the pipe. 26 | ModeReplicate BackendMode = iota 27 | 28 | // ModePush configures backend to send updates over the pipe only. 29 | // 30 | // If EnableUpdatePipe(ModePush) is called for backend, its Updates() 31 | // channel will never receive any updates. 32 | ModePush BackendMode = iota 33 | ) 34 | 35 | // The Backend interface is implemented by storage backends that support both 36 | // updates serialization using the internal updatepipe.P implementation. 37 | // To activate this implementation, EnableUpdatePipe should be called. 38 | type Backend interface { 39 | // EnableUpdatePipe enables the internal update pipe implementation. 40 | // The mode argument selects the pipe behavior. EnableUpdatePipe must be 41 | // called before the first call to the Updates() method. 42 | // 43 | // This method is idempotent. All calls after a successful one do nothing. 44 | EnableUpdatePipe(mode BackendMode) error 45 | } 46 | -------------------------------------------------------------------------------- /internal/updatepipe/pubsub/pq.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "time" 7 | 8 | "github.com/foxcpp/maddy/framework/log" 9 | "github.com/lib/pq" 10 | ) 11 | 12 | type Msg struct { 13 | Key string 14 | Payload string 15 | } 16 | 17 | type PqPubSub struct { 18 | Notify chan Msg 19 | 20 | L *pq.Listener 21 | sender *sql.DB 22 | 23 | Log log.Logger 24 | } 25 | 26 | func NewPQ(dsn string) (*PqPubSub, error) { 27 | l := &PqPubSub{ 28 | Log: log.Logger{Name: "pgpubsub"}, 29 | Notify: make(chan Msg), 30 | } 31 | l.L = pq.NewListener(dsn, 10*time.Second, time.Minute, l.eventHandler) 32 | var err error 33 | l.sender, err = sql.Open("postgres", dsn) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | go func() { 39 | defer close(l.Notify) 40 | for n := range l.L.Notify { 41 | if n == nil { 42 | continue 43 | } 44 | 45 | l.Notify <- Msg{Key: n.Channel, Payload: n.Extra} 46 | } 47 | }() 48 | 49 | return l, nil 50 | } 51 | 52 | func (l *PqPubSub) Close() error { 53 | l.sender.Close() 54 | l.L.Close() 55 | return nil 56 | } 57 | 58 | func (l *PqPubSub) eventHandler(ev pq.ListenerEventType, err error) { 59 | switch ev { 60 | case pq.ListenerEventConnected: 61 | l.Log.DebugMsg("connected") 62 | case pq.ListenerEventReconnected: 63 | l.Log.Msg("connection reestablished") 64 | case pq.ListenerEventConnectionAttemptFailed: 65 | l.Log.Error("connection attempt failed", err) 66 | case pq.ListenerEventDisconnected: 67 | l.Log.Msg("connection closed", "err", err) 68 | } 69 | } 70 | 71 | func (l *PqPubSub) Subscribe(_ context.Context, key string) error { 72 | return l.L.Listen(key) 73 | } 74 | 75 | func (l *PqPubSub) Unsubscribe(_ context.Context, key string) error { 76 | return l.L.Unlisten(key) 77 | } 78 | 79 | func (l *PqPubSub) Publish(key, payload string) error { 80 | _, err := l.sender.Exec(`SELECT pg_notify($1, $2)`, key, payload) 81 | return err 82 | } 83 | 84 | func (l *PqPubSub) Listener() chan Msg { 85 | return l.Notify 86 | } 87 | -------------------------------------------------------------------------------- /internal/updatepipe/pubsub/pubsub.go: -------------------------------------------------------------------------------- 1 | package pubsub 2 | 3 | import "context" 4 | 5 | type PubSub interface { 6 | Subscribe(ctx context.Context, key string) error 7 | Unsubscribe(ctx context.Context, key string) error 8 | Publish(key, payload string) error 9 | Listener() chan Msg 10 | Close() error 11 | } 12 | -------------------------------------------------------------------------------- /internal/updatepipe/serialize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Maddy Mail Server - Composable all-in-one email server. 3 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package updatepipe 20 | 21 | import ( 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "strconv" 26 | "strings" 27 | 28 | mess "github.com/foxcpp/go-imap-mess" 29 | ) 30 | 31 | func unescapeName(s string) string { 32 | return strings.ReplaceAll(s, "\x10", ";") 33 | } 34 | 35 | func escapeName(s string) string { 36 | return strings.ReplaceAll(s, ";", "\x10") 37 | } 38 | 39 | func parseUpdate(s string) (id string, upd *mess.Update, err error) { 40 | parts := strings.SplitN(s, ";", 2) 41 | if len(parts) != 2 { 42 | return "", nil, errors.New("updatepipe: mismatched parts count") 43 | } 44 | 45 | upd = &mess.Update{} 46 | dec := json.NewDecoder(strings.NewReader(unescapeName(parts[1]))) 47 | dec.UseNumber() 48 | err = dec.Decode(upd) 49 | if err != nil { 50 | return "", nil, fmt.Errorf("parseUpdate: %w", err) 51 | } 52 | 53 | if val, ok := upd.Key.(json.Number); ok { 54 | upd.Key, _ = strconv.ParseUint(val.String(), 10, 64) 55 | } 56 | 57 | return parts[0], upd, nil 58 | } 59 | 60 | func formatUpdate(myID string, upd mess.Update) (string, error) { 61 | updBlob, err := json.Marshal(upd) 62 | if err != nil { 63 | return "", fmt.Errorf("formatUpdate: %w", err) 64 | } 65 | return strings.Join([]string{ 66 | myID, 67 | escapeName(string(updBlob)), 68 | }, ";") + "\n", nil 69 | } 70 | -------------------------------------------------------------------------------- /maddy_debug.go: -------------------------------------------------------------------------------- 1 | //go:build debugflags 2 | // +build debugflags 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package maddy 23 | 24 | import ( 25 | _ "net/http/pprof" 26 | ) 27 | 28 | func init() { 29 | enableDebugFlags = true 30 | } 31 | -------------------------------------------------------------------------------- /signal_nonposix.go: -------------------------------------------------------------------------------- 1 | //go:build windows || plan9 2 | // +build windows plan9 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package maddy 23 | 24 | import ( 25 | "os" 26 | "os/signal" 27 | "syscall" 28 | 29 | "github.com/foxcpp/maddy/framework/log" 30 | ) 31 | 32 | func handleSignals() os.Signal { 33 | sig := make(chan os.Signal, 5) 34 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT) 35 | 36 | s := <-sig 37 | go func() { 38 | s := handleSignals() 39 | log.Printf("forced shutdown due to signal (%v)!", s) 40 | os.Exit(1) 41 | }() 42 | 43 | log.Printf("signal received (%v), next signal will force immediate shutdown.", s) 44 | return s 45 | } 46 | -------------------------------------------------------------------------------- /systemd_nonlinux.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package maddy 23 | 24 | type SDStatus string 25 | 26 | const ( 27 | SDReady = "READY=1" 28 | SDReloading = "RELOADING=1" 29 | SDStopping = "STOPPING=1" 30 | ) 31 | 32 | func systemdStatus(SDStatus, string) {} 33 | 34 | func systemdStatusErr(error) {} 35 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # maddy integration testing 2 | 3 | ## Tests structure 4 | 5 | The test library creates a temporary state and runtime directory, starts the 6 | server with the specified configuration file and lets you interact with it 7 | using a couple of convenient wrappers. 8 | 9 | ## Running 10 | 11 | To run tests, use `go test -tags integration` in this directory. Make sure to 12 | have a maddy executable in the current working directory. 13 | Use `-integration.executable` if the executable is named different or is placed 14 | somewhere else. 15 | Use `-integration.coverprofile` to pass `-test.coverprofile 16 | your_value.RANDOM` to test executable. See `./build_cover.sh` to build a 17 | server executable instrumented with coverage counters. 18 | -------------------------------------------------------------------------------- /tests/basic_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | /* 5 | Maddy Mail Server - Composable all-in-one email server. 6 | Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | package tests_test 23 | 24 | import ( 25 | "testing" 26 | 27 | "github.com/foxcpp/maddy/tests" 28 | ) 29 | 30 | func TestBasic(tt *testing.T) { 31 | tt.Parallel() 32 | 33 | // This test is mostly intended to test whether the integration testing 34 | // library is working as expected. 35 | 36 | t := tests.NewT(tt) 37 | t.DNS(nil) 38 | t.Port("smtp") 39 | t.Config(` 40 | smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} { 41 | hostname mx.maddy.test 42 | tls off 43 | 44 | deliver_to dummy 45 | }`) 46 | t.Run(1) 47 | defer t.Close() 48 | 49 | conn := t.Conn("smtp") 50 | defer conn.Close() 51 | conn.ExpectPattern("220 mx.maddy.test *") 52 | conn.Writeln("EHLO localhost") 53 | conn.ExpectPattern("250-*") 54 | conn.ExpectPattern("250-PIPELINING") 55 | conn.ExpectPattern("250-8BITMIME") 56 | conn.ExpectPattern("250-ENHANCEDSTATUSCODES") 57 | conn.ExpectPattern("250-CHUNKING") 58 | conn.ExpectPattern("250-SMTPUTF8") 59 | conn.ExpectPattern("250-SIZE *") 60 | conn.ExpectPattern("250 LIMITS RCPTMAX=20000") 61 | conn.Writeln("QUIT") 62 | conn.ExpectPattern("221 *") 63 | } 64 | -------------------------------------------------------------------------------- /tests/build_cover.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "$GO" ]; then 3 | GO=go 4 | fi 5 | exec $GO test -tags 'cover_main debugflags' -coverpkg 'github.com/foxcpp/maddy,github.com/foxcpp/maddy/pkg/...,github.com/foxcpp/maddy/internal/...' -cover -covermode atomic -c cover_test.go -o maddy.cover 6 | -------------------------------------------------------------------------------- /tests/golangci-noisy.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gosimple 4 | - structcheck 5 | - varcheck 6 | - errcheck 7 | - staticcheck 8 | - ineffassign 9 | - deadcode 10 | - typecheck 11 | - govet 12 | - unused 13 | - goimports 14 | - prealloc 15 | - unconvert 16 | - misspell 17 | - whitespace 18 | - nakedret 19 | - dogsled 20 | - godox 21 | - gocyclo 22 | - dupl 23 | - unparam 24 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ -z "$GO" ]; then 6 | export GO=go 7 | fi 8 | 9 | ./build_cover.sh 10 | 11 | clean() { 12 | rm -f /tmp/maddy-coverage-report* 13 | } 14 | trap clean EXIT 15 | 16 | $GO test -tags integration -integration.executable ./maddy.cover -integration.coverprofile /tmp/maddy-coverage-report "$@" 17 | $GO run gocovcat.go /tmp/maddy-coverage-report* > coverage.out 18 | -------------------------------------------------------------------------------- /tests/testdata/check_command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -e "${TEST_PWD}/testdata/${1}.hdr" ]; then 4 | cat "${TEST_PWD}/testdata/${1}.hdr" 5 | fi 6 | 7 | cat > ${TEST_STATE_DIR}/msg 8 | 9 | if [ -e "${TEST_PWD}/testdata/${1}.exit" ]; then 10 | exit "$(cat "${TEST_PWD}/testdata/${1}.exit")" 11 | fi 12 | -------------------------------------------------------------------------------- /tests/testdata/testing+addHeader@maddy.test.hdr: -------------------------------------------------------------------------------- 1 | X-Added-Header: 1 2 | -------------------------------------------------------------------------------- /tests/testdata/testing+reject@maddy.test.exit: -------------------------------------------------------------------------------- 1 | 12 2 | --------------------------------------------------------------------------------